Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
authorMike Rylander <mrylander@gmail.com>
Fri, 13 May 2011 15:30:25 +0000 (11:30 -0400)
committerMike Rylander <mrylander@gmail.com>
Fri, 13 May 2011 15:30:25 +0000 (11:30 -0400)
32 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
Open-ILS/src/sql/Pg/000.functions.general.sql
Open-ILS/src/sql/Pg/012.schema.vandelay.sql
Open-ILS/src/sql/Pg/030.schema.metabib.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/990.schema.unapi.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/renumber_me.sql [new file with mode: 0644]
Open-ILS/web/css/skin/default/vandelay.css
Open-ILS/web/js/dojo/fieldmapper/Fieldmapper.js
Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/widget/AutoGrid.js
Open-ILS/web/js/ui/default/conify/global/vandelay/match_set.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/vandelay/vandelay.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/locale/en-US/vandelay.dtd
Open-ILS/web/templates/default/conify/global/vandelay/match_set.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/vandelay/match_set_tree.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/vandelay/inc/attrs.tt2
Open-ILS/web/templates/default/vandelay/inc/import_errors.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/vandelay/inc/matches.tt2
Open-ILS/web/templates/default/vandelay/inc/profiles.tt2
Open-ILS/web/templates/default/vandelay/inc/queue.tt2
Open-ILS/web/templates/default/vandelay/inc/toolbar.tt2
Open-ILS/web/templates/default/vandelay/inc/upload.tt2
Open-ILS/web/templates/default/vandelay/vandelay.tt2
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul

index 724e8f5..d7c8c9d 100644 (file)
@@ -148,6 +148,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Replace Specification" name="replace_spec" reporter:datatype="text"/>
                        <field reporter:label="Remove Specification" name="strip_spec" reporter:datatype="text"/>
                        <field reporter:label="Preserve Specification" name="preserve_spec" reporter:datatype="text"/>
+                       <field reporter:label="Min. Quality Ratio" name="lwm_ratio" reporter:datatype="float"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
@@ -186,6 +187,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Import Item ID" name="id" reporter:datatype="id"/>
                        <field reporter:label="Import Record" name="record" reporter:datatype="link"/>
                        <field reporter:label="Attribute Definition" name="definition" reporter:datatype="link"/>
+                       <field reporter:label="Import Error" name="import_error" reporter:datatype="link"/>
+                       <field reporter:label="Import Error Detail" name="error_detail" reporter:datatype="text"/>
                        <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="int"/>
                        <field reporter:label="Circulating Library" name="circ_lib" reporter:datatype="int"/>
                        <field reporter:label="Call Number" name="call_number" reporter:datatype="text"/>
@@ -207,6 +210,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="bool"/>
                </fields>
                <links>
+                       <link field="import_error" reltype="has_a" key="code" map="" class="vie"/>
                        <link field="record" reltype="has_a" key="id" map="" class="vqbr"/>
                        <link field="definition" reltype="has_a" key="id" map="" class="viiad"/>
                </links>
@@ -275,11 +279,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
                        <field reporter:label="Complete" name="complete" reporter:datatype="bool"/>
                        <field reporter:label="Type" name="queue_type" reporter:datatype="text"/>
+                       <field reporter:label="Match Set" name="match_set" reporter:datatype="link"/>
                        <field reporter:label="Item Import Attribute Definition" name="item_attr_def" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="item_attr_def" reltype="has_a" key="id" map="" class="viiad"/>
+                       <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
@@ -291,6 +297,23 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </permacrud>
        </class>
 
+       <class id="vie" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="vandelay::import_error" oils_persist:tablename="vandelay.import_error" reporter:label="Import/Overlay Error Definitions">
+               <fields oils_persist:primary="code">
+                       <field reporter:label="Error Code" name="code" reporter:selector="description" reporter:datatype="id"/>
+                       <field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="item_attr_def" reltype="has_a" key="id" map="" class="viiad"/>
+                       <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve/>
+                       </actions>
+               </permacrud>
+       </class>
+
        <class id="vqbr" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="vandelay::queued_bib_record" oils_persist:tablename="vandelay.queued_bib_record" reporter:label="Queued Bib Record">
                <fields oils_persist:primary="id" oils_persist:sequence="vandelay.queued_record_id_seq">
                        <field reporter:label="Record ID" name="id" reporter:datatype="id"/>
@@ -300,16 +323,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Queue" name="queue" reporter:datatype="link"/>
                        <field reporter:label="Bib Source" name="bib_source" reporter:datatype="link"/>
                        <field reporter:label="Final Target Record" name="imported_as" reporter:datatype="link"/>
+                       <field reporter:label="Import Error" name="import_error" reporter:datatype="link"/>
+                       <field reporter:label="Import Error Detail" name="error_detail" reporter:datatype="text"/>
                        <field reporter:label="Purpose" name="purpose" reporter:datatype="text"/>
                        <field reporter:label="Attributes" name="attributes" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Matches" name="matches" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Import Items" name="import_items" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Quality" name="quality" reporter:datatype="int"/>
                </fields>
                <links>
+                       <link field="import_error" reltype="has_a" key="code" map="" class="vie"/>
                        <link field="queue" reltype="has_a" key="id" map="" class="vbq"/>
                        <link field="bib_source" reltype="has_a" key="id" map="" class="cbs"/>
                        <link field="imported_as" reltype="has_a" key="id" map="" class="bre"/>
             <link field="attributes" reltype="has_many" key="record" map="" class="vqbra"/>
             <link field="matches" reltype="has_many" key="queued_record" map="" class="vbm"/>
+            <link field="import_items" reltype="has_many" key="record" map="" class="vii"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
@@ -328,7 +357,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
                        <field reporter:label="XPath" name="xpath" reporter:datatype="text"/>
                        <field reporter:label="Remove RegExp" name="remove" reporter:datatype="text"/>
-                       <field reporter:label="Is Identifier?" name="ident" reporter:datatype="bool"/>
                </fields>
                <links/>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -366,14 +394,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                <fields oils_persist:primary="id" oils_persist:sequence="vandelay.bib_match_id_seq">
                        <field reporter:label="Match ID" name="id" reporter:datatype="id"/>
                        <field reporter:label="Queued Record" name="queued_record" reporter:datatype="link"/>
-                       <field reporter:label="Matched Attribute" name="matched_attr" reporter:datatype="link"/>
                        <field reporter:label="Evergreen Record" name="eg_record" reporter:datatype="link"/>
-                       <field reporter:label="Field Type" name="field_type" reporter:datatype="text"/>
+                       <field reporter:label="Quality" name="quality" reporter:datatype="text"/>
+                       <field reporter:label="Match Score" name="match_score" reporter:datatype="text"/>
                </fields>
                <links>
                        <link field="queued_record" reltype="has_a" key="id" map="" class="vqbr"/>
                        <link field="eg_record" reltype="has_a" key="id" map="" class="bre"/>
-                       <link field="matched_attr" reltype="has_a" key="id" map="" class="vqbra"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
@@ -392,9 +419,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
                        <field reporter:label="Complete" name="complete" reporter:datatype="bool"/>
                        <field reporter:label="Type" name="queue_type" reporter:datatype="text"/>
+                       <field reporter:label="Match Set" name="match_set" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
@@ -414,11 +443,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="MARC" name="marc" reporter:datatype="text"/>
                        <field reporter:label="Queue" name="queue" reporter:datatype="link"/>
                        <field reporter:label="Final Target Record" name="imported_as" reporter:datatype="link"/>
+                       <field reporter:label="Import Error" name="import_error" reporter:datatype="link"/>
+                       <field reporter:label="Import Error Detail" name="error_detail" reporter:datatype="text"/>
                        <field reporter:label="Purpose" name="purpose" reporter:datatype="text"/>
                        <field reporter:label="Attributes" name="attributes" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Matches" name="matches" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Quality" name="quality" reporter:datatype="int"/>
                </fields>
                <links>
+                       <link field="import_error" reltype="has_a" key="code" map="" class="vie"/>
                        <link field="queue" reltype="has_a" key="id" map="" class="vaq"/>
                        <link field="imported_as" reltype="has_a" key="id" map="" class="are"/>
             <link field="attributes" reltype="has_many" key="record" map="" class="vqara"/>
@@ -441,7 +474,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
                        <field reporter:label="XPath" name="xpath" reporter:datatype="text"/>
                        <field reporter:label="Remove RegExp" name="remove" reporter:datatype="text"/>
-                       <field reporter:label="Is Identifier?" name="ident" reporter:datatype="bool"/>
                </fields>
                <links/>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -479,13 +511,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                <fields oils_persist:primary="id" oils_persist:sequence="vandelay.authority_match_id_seq">
                        <field reporter:label="Match ID" name="id" reporter:datatype="id"/>
                        <field reporter:label="Queued Record" name="queued_record" reporter:datatype="link"/>
-                       <field reporter:label="Matched Attribute" name="matched_attr" reporter:datatype="link"/>
                        <field reporter:label="Evergreen Record" name="eg_record" reporter:datatype="link"/>
+                       <field reporter:label="Quality" name="quality" reporter:datatype="int"/>
                </fields>
                <links>
                        <link field="queued_record" reltype="has_a" key="id" map="" class="vqbr"/>
                        <link field="eg_record" reltype="has_a" key="id" map="" class="bre"/>
-                       <link field="matched_attr" reltype="has_a" key="id" map="" class="vqbra"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
@@ -497,6 +528,93 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </permacrud>
        </class>
 
+       <class id="vms" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="vandelay::match_set" oils_persist:tablename="vandelay.match_set" reporter:label="Record Matching Definition Set">
+               <fields oils_persist:primary="id" oils_persist:sequence="vandelay.match_set_id_seq">
+                       <field reporter:label="Match Set ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Name" name="name" reporter:datatype="text"/>
+                       <field reporter:label="Owning Library" name="owner" reporter:datatype="link"/>
+                       <field reporter:label="Match Set Type" name="mtype" reporter:datatype="text"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <update permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                               <delete permission="ADMIN_IMPORT_MATCH_SET" context_field="owner"/>
+                       </actions>
+               </permacrud>
+       </class>
+
+       <class id="vmsp" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="vandelay::match_set_point" oils_persist:tablename="vandelay.match_set_point" reporter:label="Record Matching Definition">
+               <fields oils_persist:primary="id" oils_persist:sequence="vandelay.match_set_point_id_seq">
+                       <field reporter:label="Match Definition ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Expression Tree Parent" name="parent" reporter:datatype="link"/>
+                       <field reporter:label="Match Set" name="match_set" reporter:datatype="link"/>
+                       <field reporter:label="Boolean Operator" name="bool_op" reporter:datatype="text"/>
+                       <field reporter:label="Coded Field" name="svf" reporter:datatype="link"/>
+                       <field reporter:label="Tag" name="tag" reporter:datatype="text"/>
+                       <field reporter:label="Subfield" name="subfield" reporter:datatype="text"/>
+            <field reporter:label="Negate" name="negate"  reporter:datatype="bool"/>
+                       <field reporter:label="Importance" name="quality" reporter:datatype="int"/>
+                       <field reporter:label="Expression Tree Children" name="children" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="parent" reltype="has_a" key="id" map="" class="vmsp"/>
+                       <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
+                       <link field="svf" reltype="has_a" key="id" map="" class="crad"/>
+                       <link field="children" reltype="has_many" key="parent" map="" class="vmsp"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </create>
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </retrieve>
+                               <update permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </update>
+                               <delete permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </delete>
+                       </actions>
+               </permacrud>
+       </class>
+
+       <class id="vmsq" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="vandelay::match_set_quality" oils_persist:tablename="vandelay.match_set_quality" reporter:label="Record Quality Metric">
+               <fields oils_persist:primary="id" oils_persist:sequence="vandelay.match_set_quality_id_seq">
+                       <field reporter:label="Quality Metric ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Match Set" name="match_set" reporter:datatype="link"/>
+                       <field reporter:label="Record Attribute" name="svf" reporter:datatype="text"/>
+                       <field reporter:label="Tag" name="tag" reporter:datatype="text"/>
+                       <field reporter:label="Subfield" name="subfield" reporter:datatype="text"/>
+                       <field reporter:label="Value" name="value" reporter:datatype="text"/>
+                       <field reporter:label="Quality" name="quality" reporter:datatype="int"/>
+               </fields>
+               <links>
+                       <link field="match_set" reltype="has_a" key="id" map="" class="vms"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </create>
+                               <retrieve permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </retrieve>
+                               <update permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </update>
+                               <delete permission="ADMIN_IMPORT_MATCH_SET">
+                    <context link="match_set" field="owner"/>
+                               </delete>
+                       </actions>
+               </permacrud>
+       </class>
 
        <class id="auoi" controller="open-ils.cstore" oils_obj:fieldmapper="actor::usr_org_unit_opt_in" oils_persist:tablename="actor.usr_org_unit_opt_in" reporter:label="User Sharing Opt-in">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.usr_org_unit_opt_in_id_seq">
index 4251519..cfeeca6 100644 (file)
@@ -148,6 +148,57 @@ my $_TT_helpers = {
         }
         return;
     },
+
+    get_queued_bib_attr => sub {
+        my $name = shift or return;     # the first arg is always the name
+        my ($attr) = @_;
+        # use Data::Dumper; $logger->warn("get_queued_bib_attr: " . Dumper($attr));
+        ($name and @$attr) or return;
+
+        my $query = {
+            select => {'vqbrad' => ['id']},
+            from => 'vqbrad',
+            where => {code => $name}
+        };
+
+        my $def_ids = new_editor()->json_query($query);
+        @$def_ids or return;
+
+        my $length;
+        $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
+        foreach (@$attr) {
+            $_->field eq @{$def_ids}[0]->{id} or next;
+            next if $length and $length != length($_->attr_value);
+            return $_->attr_value;
+        }
+        return;
+    },
+
+    get_queued_auth_attr => sub {
+        my $name = shift or return;     # the first arg is always the name
+        my ($attr) = @_;
+        # use Data::Dumper; $logger->warn("get_queued_auth_attr: " . Dumper($attr));
+        ($name and @$attr) or return;
+
+        my $query = {
+            select => {'vqarad' => ['id']},
+            from => 'vqarad',
+            where => {code => $name}
+        };
+
+        my $def_ids = new_editor()->json_query($query);
+        @$def_ids or return;
+
+        my $length;
+        $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
+        foreach (@$attr) {
+            $_->field eq @{$def_ids}[0]->{id} or next;
+            next if $length and $length != length($_->attr_value);
+            return $_->attr_value;
+        }
+        return;
+    },
+
 };
 
 
index 5c48282..4a0bf99 100644 (file)
@@ -60,6 +60,7 @@ sub create_bib_queue {
     my $name = shift;
     my $owner = shift;
     my $type = shift;
+    my $match_set = shift;
     my $import_def = shift;
 
     my $e = new_editor(authtoken => $auth, xact => 1);
@@ -78,6 +79,7 @@ sub create_bib_queue {
     $queue->owner( $owner );
     $queue->queue_type( $type ) if ($type);
     $queue->item_attr_def( $import_def ) if ($import_def);
+    $queue->match_set($match_set) if $match_set;
 
     my $new_q = $e->create_vandelay_bib_queue( $queue );
     return $e->die_event unless ($new_q);
@@ -100,6 +102,7 @@ sub create_auth_queue {
     my $name = shift;
     my $owner = shift;
     my $type = shift;
+    my $match_set = shift;
 
     my $e = new_editor(authtoken => $auth, xact => 1);
 
@@ -366,6 +369,31 @@ __PACKAGE__->register_method(
     stream      => 1,
     record_type => 'bib'
 );
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.bib_queue.records.retrieve.export.print",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'bib'
+);
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.bib_queue.records.retrieve.export.csv",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'bib'
+);
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.bib_queue.records.retrieve.export.email",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'bib'
+);
+
 __PACKAGE__->register_method(  
     api_name    => "open-ils.vandelay.auth_queue.records.retrieve",
     method      => 'retrieve_queued_records',
@@ -374,6 +402,30 @@ __PACKAGE__->register_method(
     stream      => 1,
     record_type => 'auth'
 );
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.auth_queue.records.retrieve.export.print",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'auth'
+);
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.auth_queue.records.retrieve.export.csv",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'auth'
+);
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.auth_queue.records.retrieve.export.email",
+    method      => 'retrieve_queued_records',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    record_type => 'auth'
+);
 
 __PACKAGE__->register_method(  
     api_name    => "open-ils.vandelay.bib_queue.records.matches.retrieve",
@@ -396,18 +448,19 @@ __PACKAGE__->register_method(
     signature   => {
         desc => q/Only retrieve queued authority records that have matches against existing records/
     }
-
 );
 
 sub retrieve_queued_records {
     my($self, $conn, $auth, $queue_id, $options) = @_;
-    my $e = new_editor(authtoken => $auth, xact => 1);
-    return $e->die_event unless $e->checkauth;
+
     $options ||= {};
     my $limit = $$options{limit} || 20;
     my $offset = $$options{offset} || 0;
-
     my $type = $self->{record_type};
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth;
+
     my $queue;
     if($type eq 'bib') {
         $queue = $e->retrieve_vandelay_bib_queue($queue_id) or return $e->die_event;
@@ -418,38 +471,245 @@ sub retrieve_queued_records {
     return $evt if ($evt);
 
     my $class = ($type eq 'bib') ? 'vqbr' : 'vqar';
-    my $search = ($type eq 'bib') ? 
-        'search_vandelay_queued_bib_record' : 'search_vandelay_queued_authority_record';
+    my $query = {
+        select => {$class => ['id']},
+        from => $class,
+        where => {queue => $queue_id},
+        distinct => 1,
+        order_by => {$class => ['id']}, 
+        limit => $limit,
+        offset => $offset,
+    };
+    if($self->api_name =~ /export/) {
+        delete $query->{limit};
+        delete $query->{offset};
+    }
+
+    $query->{where}->{import_time} = undef if $$options{non_imported};
+
+    if($$options{with_import_error}) {
+
+        $query->{from} = {$class => {vii => {type => 'right'}}};
+        $query->{where}->{'-or'} = [
+            {'+vqbr' => {import_error => {'!=' => undef}}},
+            {'+vii' => {import_error => {'!=' => undef}}}
+        ];
+
+    } else {
+        
+        if($$options{with_rec_import_error}) {
+            $query->{where}->{import_error} = {'!=' => undef};
+
+        } elsif( $$options{with_item_import_error} and $type eq 'bib') {
+
+            $query->{from} = {$class => 'vii'};
+            $query->{where}->{'+vii'} = {import_error => {'!=' => undef}};
+        }
+    }
+
+    if($self->api_name =~ /matches/) {
+        # find only records that have matches
+        my $mclass = $type eq 'bib' ? 'vbm' : 'vam';
+        $query->{from} = {$class => {$mclass => {type => 'right'}}};
+    } 
+
+    my $record_ids = $e->json_query($query);
+
     my $retrieve = ($type eq 'bib') ? 
         'retrieve_vandelay_queued_bib_record' : 'retrieve_vandelay_queued_authority_record';
+    my $search = ($type eq 'bib') ? 
+        'search_vandelay_queued_bib_record' : 'search_vandelay_queued_authority_record';
 
-    my $filter = ($$options{non_imported}) ? {import_time => undef} : {};
+    if ($self->api_name =~ /export/) {
+        my $rec_list = $e->$search({id => [map { $_->{id} } @$record_ids]});
+        if ($self->api_name =~ /print/) {
 
-    my $record_ids;
-    if($self->api_name =~ /matches/) {
-        # fetch only matched records
-        $record_ids = queued_records_with_matches($e, $type, $queue_id, $limit, $offset, $filter);
+            $e->rollback;
+            return $U->fire_object_event(
+                undef,
+                'vandelay.queued_'.$type.'_record.print',
+                $rec_list,
+                $e->requestor->ws_ou
+            );
+
+        } elsif ($self->api_name =~ /csv/) {
+
+            $e->rollback;
+            return $U->fire_object_event(
+                undef,
+                'vandelay.queued_'.$type.'_record.csv',
+                $rec_list,
+                $e->requestor->ws_ou
+            );
+
+        } elsif ($self->api_name =~ /email/) {
+
+            $conn->respond_complete(1);
+
+            for my $rec (@$rec_list) {
+                $U->create_events_for_hook(
+                    'vandelay.queued_'.$type.'_record.email',
+                    $rec,
+                    $e->requestor->home_ou,
+                    undef,
+                    undef,
+                    1
+                );
+            }
+
+        }
     } else {
-        # fetch all queue records
-        $record_ids = $e->$search([
-                {queue => $queue_id, %$filter}, 
-                {order_by => {$class => 'id'}, limit => $limit, offset => $offset}
-            ],
-            {idlist => 1}
-        );
+        for my $rec_id (@$record_ids) {
+            my $flesh = ['attributes', 'matches'];
+            push(@$flesh, 'import_items') if $$options{flesh_import_items};
+            my $params = {flesh => 1, flesh_fields => {$class => $flesh}};
+            my $rec = $e->$retrieve([$rec_id->{id}, $params]);
+            $rec->clear_marc if $$options{clear_marc};
+            $conn->respond($rec);
+        }
     }
 
+    $e->rollback;
+    return undef;
+}
 
-    for my $rec_id (@$record_ids) {
-        my $params = {   
-            flesh => 1,
-            flesh_fields => {$class => ['attributes', 'matches']},
-        };
-        my $rec = $e->$retrieve([$rec_id, $params]);
-        $rec->clear_marc if $$options{clear_marc};
-        $conn->respond($rec);
+__PACKAGE__->register_method(  
+    api_name    => 'open-ils.vandelay.import_item.queue.retrieve',
+    method      => 'retrieve_queue_import_items',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    authoritative => 1,
+    signature => q/
+        Returns Import Item (vii) objects for the selected queue.
+        Filter options:
+            with_import_error : only return items that failed to import
+    /
+);
+__PACKAGE__->register_method(
+    api_name    => 'open-ils.vandelay.import_item.queue.export.print',
+    method      => 'retrieve_queue_import_items',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    authoritative => 1,
+    signature => q/
+        Returns template-generated printable output of Import Item (vii) objects for the selected queue.
+        Filter options:
+            with_import_error : only return items that failed to import
+    /
+);
+__PACKAGE__->register_method(
+    api_name    => 'open-ils.vandelay.import_item.queue.export.csv',
+    method      => 'retrieve_queue_import_items',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    authoritative => 1,
+    signature => q/
+        Returns template-generated CSV output of Import Item (vii) objects for the selected queue.
+        Filter options:
+            with_import_error : only return items that failed to import
+    /
+);
+__PACKAGE__->register_method(
+    api_name    => 'open-ils.vandelay.import_item.queue.export.email',
+    method      => 'retrieve_queue_import_items',
+    api_level   => 1,
+    argc        => 2,
+    stream      => 1,
+    authoritative => 1,
+    signature => q/
+        Emails template-generated output of Import Item (vii) objects for the selected queue.
+        Filter options:
+            with_import_error : only return items that failed to import
+    /
+);
+
+sub retrieve_queue_import_items {
+    my($self, $conn, $auth, $q_id, $options) = @_;
+
+    $options ||= {};
+    my $limit = $$options{limit} || 20;
+    my $offset = $$options{offset} || 0;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my $queue = $e->retrieve_vandelay_bib_queue($q_id) or return $e->event;
+    my $evt = check_queue_perms($e, 'bib', $queue);
+    return $evt if $evt;
+
+    my $query = {
+        select => {vii => ['id']},
+        from => {
+            vii => {
+                vqbr => {
+                    join => {
+                        'vbq' => {
+                            field => 'id',
+                            fkey => 'queue',
+                            filter => {id => $q_id}
+                        }
+                    }
+                }
+            }
+        },
+        order_by => {'vii' => ['record','id']},
+        limit => $limit,
+        offset => $offset
+    };
+    if($self->api_name =~ /export/) {
+        delete $query->{limit};
+        delete $query->{offset};
     }
-    $e->rollback;
+
+    $query->{where} = {'+vii' => {import_error => {'!=' => undef}}}
+        if $$options{with_import_error};
+
+    my $items = $e->json_query($query);
+    my $item_list = $e->search_vandelay_import_item({id => [map { $_->{id} } @$items]});
+    if ($self->api_name =~ /export/) {
+        if ($self->api_name =~ /print/) {
+
+            return $U->fire_object_event(
+                undef,
+                'vandelay.import_items.print',
+                $item_list,
+                $e->requestor->ws_ou
+            );
+
+        } elsif ($self->api_name =~ /csv/) {
+
+            return $U->fire_object_event(
+                undef,
+                'vandelay.import_items.csv',
+                $item_list,
+                $e->requestor->ws_ou
+            );
+
+        } elsif ($self->api_name =~ /email/) {
+
+            $conn->respond_complete(1);
+
+            for my $item (@$item_list) {
+                $U->create_events_for_hook(
+                    'vandelay.import_items.email',
+                    $item,
+                    $e->requestor->home_ou,
+                    undef,
+                    undef,
+                    1
+                );
+            }
+
+        }
+    } else {
+        for my $item (@$item_list) {
+            $conn->respond($item);
+        }
+    }
+
     return undef;
 }
 
@@ -492,7 +752,7 @@ sub import_record_list {
     return $e->die_event unless $e->checkauth;
     $args ||= {};
     my $err = import_record_list_impl($self, $conn, $rec_ids, $e->requestor, $args);
-    $e->rollback;
+    try {$e->rollback} otherwise {}; 
     return $err if $err;
     return {complete => 1};
 }
@@ -606,25 +866,33 @@ sub queued_records_with_matches {
     return [ map {$_->{queued_record}} @$data ];
 }
 
+
 sub import_record_list_impl {
     my($self, $conn, $rec_ids, $requestor, $args) = @_;
 
     my $overlay_map = $args->{overlay_map} || {};
     my $type = $self->{record_type};
-    my $total = @$rec_ids;
-    my $count = 0;
     my %queues;
 
-    my $step = 1;
+    my $report_args = {
+        progress => 1,
+        step => 1,
+        conn => $conn,
+        total => scalar(@$rec_ids),
+        report_all => $$args{report_all}
+    };
 
     my $auto_overlay_exact = $$args{auto_overlay_exact};
     my $auto_overlay_1match = $$args{auto_overlay_1match};
+    my $auto_overlay_best = $$args{auto_overlay_best_match};
+    my $match_quality_ratio = $$args{match_quality_ratio};
     my $merge_profile = $$args{merge_profile};
     my $bib_source = $$args{bib_source};
-    my $report_all = $$args{report_all};
+    my $import_no_match = $$args{import_no_match};
 
     my $overlay_func = 'vandelay.overlay_bib_record';
     my $auto_overlay_func = 'vandelay.auto_overlay_bib_record';
+    my $auto_overlay_best_func = 'vandelay.auto_overlay_bib_record_with_best'; # XXX bib-only
     my $retrieve_func = 'retrieve_vandelay_queued_bib_record';
     my $update_func = 'update_vandelay_queued_bib_record';
     my $search_func = 'search_vandelay_queued_bib_record';
@@ -632,13 +900,11 @@ sub import_record_list_impl {
     my $update_queue_func = 'update_vandelay_bib_queue';
     my $rec_class = 'vqbr';
 
-    my %bib_sources;
     my $editor = new_editor();
-    my $sources = $editor->search_config_bib_source({id => {'!=' => undef}});
 
-    foreach my $src (@$sources) {
-        $bib_sources{$src->id} = $src->source;
-    }
+    my %bib_sources;
+    my $sources = $editor->search_config_bib_source({id => {'!=' => undef}});
+    $bib_sources{$_->id} = $_->source for @$sources;
 
     if($type eq 'auth') {
         $overlay_func =~ s/bib/auth/o;
@@ -654,12 +920,16 @@ sub import_record_list_impl {
     my @success_rec_ids;
     for my $rec_id (@$rec_ids) {
 
+        my $error = 0;
         my $overlay_target = $overlay_map->{$rec_id};
 
-        my $error = 0;
         my $e = new_editor(xact => 1);
         $e->requestor($requestor);
 
+        $$report_args{e} = $e;
+        $$report_args{import_error} = undef;
+        $$report_args{evt} = undef;
+
         my $rec = $e->$retrieve_func([
             $rec_id,
             {   flesh => 1,
@@ -668,11 +938,13 @@ sub import_record_list_impl {
         ]);
 
         unless($rec) {
-            $conn->respond({total => $total, progress => ++$count, imported => $rec_id, err_event => $e->event});
-            $e->rollback;
+            $$report_args{evt} = $e->event;
+            finish_rec_import_attempt($report_args);
             next;
         }
 
+        $$report_args{rec} = $rec;
+
         if($rec->import_time) {
             $e->rollback;
             next;
@@ -763,6 +1035,8 @@ sub import_record_list_impl {
                     if($res->{$auto_overlay_func} eq 't') {
                         $logger->info("vl: $type auto-overlay succeeded for queued rec " . $rec->id);
                         $imported = 1;
+                    } else {
+                        $logger->info("vl: $type auto-overlay failed for queued rec " . $rec->id);
                     }
 
                 } else {
@@ -771,10 +1045,43 @@ sub import_record_list_impl {
                 }
             }
 
-            if(!$imported and !$error) {
+            if(!$imported and !$error and $auto_overlay_best and scalar(@{$rec->matches}) > 0 ) {
+
+                # caller says to overlay the best match
+
+                my $res = $e->json_query(
+                    {
+                        from => [
+                            $auto_overlay_best_func,
+                            $rec->id, 
+                            $merge_profile,
+                            $match_quality_ratio
+                        ]
+                    }
+                );
+
+                if($res and ($res = $res->[0])) {
+
+                    if($res->{$auto_overlay_best_func} eq 't') {
+                        $logger->info("vl: $type auto-overlay-best succeeded for queued rec " . $rec->id);
+                        $imported = 1;
+                    } else {
+                        $$report_args{import_error} = 'overlay.record.quality' if $match_quality_ratio > 0;
+                        $logger->info("vl: $type auto-overlay-best failed for queued rec " . $rec->id);
+                    }
+
+                } else {
+                    $error = 1;
+                    $logger->error("vl: Error attempting overlay with func=$auto_overlay_best_func, ".
+                        "quality_ratio=$match_quality_ratio, profile=$merge_profile, record=$rec_id");
+                }
+            }
+
+            if(!$imported and !$error and $import_no_match and scalar(@{$rec->matches}) == 0) {
             
                 # No overlay / merge occurred.  Do a traditional record import by creating a new record
             
+                $logger->info("vl: creating new $type record for queued record $rec_id");
                 if($type eq 'bib') {
                     $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import($e, $rec->marc, $bib_sources{$rec->bib_source});
                 } else {
@@ -783,16 +1090,16 @@ sub import_record_list_impl {
                 }
 
                 if($U->event_code($record)) {
-
-                    $e->event($record); 
-                    $e->rollback;
+                    $$report_args{import_error} = 'import.duplicate.tcn' if $record->{textcode} eq 'TCN_EXISTS';
+                    $$report_args{evt} = $record;
 
                 } else {
 
                     $logger->info("vl: successfully imported new $type record");
                     $rec->imported_as($record->id);
                     $rec->import_time('now');
-
+                    $rec->clear_import_error;
+                    $rec->clear_error_detail;
                     $imported = 1 if $e->$update_func($rec);
                 }
             }
@@ -800,16 +1107,12 @@ sub import_record_list_impl {
 
         if($imported) {
             push @success_rec_ids, $rec_id;
-            $e->commit;
+            finish_rec_import_attempt($report_args);
+
         } else {
             # Send an update whenever there's an error
-            $conn->respond({total => $total, progress => ++$count, imported => $rec_id, err_event => $e->event});
-        }
-
-        if($report_all or (++$count % $step) == 0) {
-            $conn->respond({total => $total, progress => $count, imported => $rec_id});
-            # report often at first, climb quickly, then hold steady
-            $step *= 2 unless $step == 256;
+            $$report_args{evt} = $e->event unless $$report_args{evt};
+            finish_rec_import_attempt($report_args);
         }
     }
 
@@ -833,12 +1136,70 @@ sub import_record_list_impl {
        $e->rollback;
     }
 
+    # import the copies
     import_record_asset_list_impl($conn, \@success_rec_ids, $requestor);
 
-    $conn->respond({total => $total, progress => $count});
+    $conn->respond({total => $$report_args{total}, progress => $$report_args{progress}});
     return undef;
 }
 
+# tracks any import errors, commits the current xact, responds to the client
+sub finish_rec_import_attempt {
+    my $args = shift;
+    my $evt = $$args{evt};
+    my $rec = $$args{rec};
+    my $e = $$args{e};
+
+    my $error = $$args{import_error};
+    $error = 'general.unknown' if $evt and not $error;
+
+    # error tracking
+    if($rec) {
+
+        if($error or $evt) {
+            # failed import
+            # since an error occurred, there's no guarantee the transaction wasn't 
+            # rolled back.  force a rollback and create a new editor.
+            $e->rollback;
+            $e = new_editor(xact => 1);
+            $rec->import_error($error);
+
+            if($evt) {
+                my $detail = sprintf("%s : %s", $evt->{textcode}, substr($evt->{desc}, 0, 140));
+                $rec->error_detail($detail);
+            }
+
+            my $method = 'update_vandelay_queued_bib_record';
+            $method =~ s/bib/authority/ if $$args{type} eq 'auth';
+            $e->$method($rec) and $e->commit or $e->rollback;
+
+        } else {
+            # commit the successful import
+            $e->commit;
+        }
+
+    } else {
+        # requested queued record was not found
+        $e->rollback;
+    }
+        
+    # respond to client
+    if($$args{report_all} or ($$args{progress} % $$args{step}) == 0) {
+        $$args{conn}->respond({
+            total => $$args{total}, 
+            progress => $$args{progress}, 
+            imported => ($rec) ? $rec->id : undef,
+            err_event => $evt
+        });
+        $$args{step} *= 2 unless $$args{step} == 256;
+    }
+
+    $$args{progress}++;
+}
+
+
+
+
 
 __PACKAGE__->register_method(  
     api_name    => "open-ils.vandelay.bib_queue.owner.retrieve",
@@ -991,11 +1352,41 @@ sub retrieve_queue_summary {
     my $search = 'search_vandelay_queued_bib_record';
     $search =~ s/bib/authority/ if $type ne 'bib';
 
-    return {
+    my $summary = {
         queue => $queue,
         total => scalar(@{$e->$search({queue => $queue_id}, {idlist=>1})}),
         imported => scalar(@{$e->$search({queue => $queue_id, import_time => {'!=' => undef}}, {idlist=>1})}),
     };
+
+    my $class = ($type eq 'bib') ? 'vqbr' : 'vqar';
+    $summary->{rec_import_errors} = $e->json_query({
+        select => {$class => [{alias => 'count', column => 'id', transform => 'count', aggregate => 1}]},
+        from => $class,
+        where => {queue => $queue_id, import_error => {'!=' => undef}}
+    })->[0]->{count};
+
+    if($type eq 'bib') {
+        
+        my $query = {
+            select => {vii => [{alias => 'count', column => 'id', transform => 'count', aggregate => 1}]},
+            from => 'vii',
+            where => {
+                record => {
+                    in => {
+                        select => {vqbr => ['id']},
+                        from => 'vqbr',
+                        where => {queue => $queue_id}
+                    }
+                }
+            }
+        };
+
+        $summary->{total_items} = $e->json_query($query)->[0]->{count};
+        $query->{where}->{import_error} = {'!=' => undef};
+        $summary->{item_import_errors} = $e->json_query($query)->[0]->{count};
+    }
+
+    return $summary;
 }
 
 # --------------------------------------------------------------------------------
@@ -1004,20 +1395,37 @@ sub retrieve_queue_summary {
 sub import_record_asset_list_impl {
     my($conn, $rec_ids, $requestor) = @_;
 
-    my $total = @$rec_ids;
-    my $try_count = 0;
-    my $in_count = 0;
     my $roe = new_editor(xact=> 1, requestor => $requestor);
 
+    # for speed, filter out any records have not been 
+    # imported or have no import items to load
+    $rec_ids = $roe->json_query({
+        select => {vqbr => ['id']},
+        from => {vqbr => 'vii'},
+        where => {'+vqbr' => {import_time => {'!=' => undef}}},
+        distinct => 1
+    });
+    $rec_ids = [map {$_->{id}} @$rec_ids];
+
+    my $report_args = {
+        conn => $conn,
+        total => scalar(@$rec_ids),
+        step => 1, # how often to respond
+        progress => 1,
+        in_count => 0,
+    };
+
     for my $rec_id (@$rec_ids) {
         my $rec = $roe->retrieve_vandelay_queued_bib_record($rec_id);
-        next unless $rec and $rec->import_time;
         my $item_ids = $roe->search_vandelay_import_item({record => $rec->id}, {idlist=>1});
 
         for my $item_id (@$item_ids) {
             my $e = new_editor(requestor => $requestor, xact => 1);
             my $item = $e->retrieve_vandelay_import_item($item_id);
-            $try_count++;
+            $$report_args{import_item} = $item;
+            $$report_args{e} = $e;
+            $$report_args{import_error} = undef;
+            $$report_args{evt} = undef;
 
             # --------------------------------------------------------------------------------
             # Find or create the volume
@@ -1027,7 +1435,9 @@ sub import_record_asset_list_impl {
                     $e, $item->call_number, $rec->imported_as, $item->owning_lib);
 
             if($evt) {
-                respond_with_status($conn, $total, $try_count, $in_count, $evt);
+
+                $$report_args{evt} = $evt;
+                respond_with_status($report_args);
                 next;
             }
 
@@ -1055,15 +1465,25 @@ sub import_record_asset_list_impl {
             # --------------------------------------------------------------------------------
             # see if a valid circ_modifier was provided
             # --------------------------------------------------------------------------------
-            #if($copy->circ_modifier and not $e->retrieve_config_circ_modifier($item->circ_modifier)) {
             if($copy->circ_modifier and not $e->search_config_circ_modifier({code=>$item->circ_modifier})->[0]) {
-                respond_with_status($conn, $total, $try_count, $in_count, $e->die_event);
+                $$report_args{evt} = $e->die_event;
+                $$report_args{import_error} = 'import.item.invalid.circ_modifier';
+                respond_with_status($report_args);
+                next;
+            }
+
+            if($copy->location and not $e->retrieve_asset_copy_location($copy->location)) {
+                $$report_args{evt} = $e->die_event;
+                $$report_args{import_error} = 'import.item.invalid.location';
+                respond_with_status($report_args);
                 next;
             }
 
             if($evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $vol, $copy)) {
-                try { $e->rollback } otherwise {}; # sometimes calls die_event, sometimes not
-                respond_with_status($conn, $total, $try_count, $in_count, $evt);
+                $$report_args{evt} = $evt;
+                $$report_args{import_error} = 'import.item.duplicate.barcode'
+                    if $evt->{textcode} eq 'ITEM_BARCODE_EXISTS';
+                respond_with_status($report_args);
                 next;
             }
 
@@ -1074,7 +1494,8 @@ sub import_record_asset_list_impl {
                 $e, $copy, '', $item->pub_note, 1) if $item->pub_note;
 
             if($evt) {
-                respond_with_status($conn, $total, $try_count, $in_count, $evt);
+                $$report_args{evt} = $evt;
+                respond_with_status($report_args);
                 next;
             }
 
@@ -1082,7 +1503,8 @@ sub import_record_asset_list_impl {
                 $e, $copy, '', $item->priv_note, 1) if $item->priv_note;
 
             if($evt) {
-                respond_with_status($conn, $total, $try_count, $in_count, $evt);
+                $$report_args{evt} = $evt;
+                respond_with_status($report_args);
                 next;
             }
 
@@ -1090,22 +1512,156 @@ sub import_record_asset_list_impl {
             # Item import succeeded
             # --------------------------------------------------------------------------------
             $e->commit;
-            respond_with_status($conn, $total, $try_count, ++$in_count, undef, imported_as => $copy->id);
+            $$report_args{in_count}++;
+            respond_with_status($report_args);
+            $logger->info("vl: successfully imported item " . $item->barcode);
         }
+
     }
+
     $roe->rollback;
     return undef;
 }
 
 
 sub respond_with_status {
-    my($conn, $total, $try_count, $success_count, $err, %args) = @_;
-    $conn->respond({
-        total => $total, 
-        progress => $try_count, 
-        err_event => $err, 
-        success_count => $success_count, %args }) if $err or ($try_count % 5 == 0);
+    my $args = shift;
+    my $e = $$args{e};
+
+    #  If the import failed, track the failure reason
+
+    my $error = $$args{import_error};
+    my $evt = $$args{evt};
+
+    if($error or $evt) {
+
+        my $item = $$args{import_item};
+        $logger->info("vl: unable to import item " . $item->barcode);
+
+        $error ||= 'general.unknown';
+        $item->import_error($error);
+
+        if($evt) {
+            my $detail = sprintf("%s : %s", $evt->{textcode}, substr($evt->{desc}, 0, 140));
+            $item->error_detail($detail);
+        }
+
+        # state of the editor is unknown at this point.  Force a rollback and start over.
+        $e->rollback;
+        $e = new_editor(xact => 1);
+        $e->update_vandelay_import_item($item);
+        $e->commit;
+    }
+
+    if($$args{report_all} or ($$args{progress} % $$args{step}) == 0) {
+        $$args{conn}->respond({
+            map { $_ => $args->{$_} } qw/total progress success_count/,
+            err_event => $evt
+        });
+        $$args{step} *= 2 unless $$args{step} == 256;
+    }
+
+    $$args{progress}++;
 }
 
+__PACKAGE__->register_method(  
+    api_name    => "open-ils.vandelay.match_set.get_tree",
+    method      => "match_set_get_tree",
+    api_level   => 1,
+    argc        => 2,
+    signature   => {
+        desc    => q/For a given vms object, return a tree of match set points
+                    represented by a vmsp object with recursively fleshed
+                    children./
+    }
+);
+
+sub match_set_get_tree {
+    my ($self, $conn, $authtoken, $match_set_id) = @_;
+
+    $match_set_id = int($match_set_id) or return;
+
+    my $e = new_editor("authtoken" => $authtoken);
+    $e->checkauth or return $e->die_event;
+
+    my $set = $e->retrieve_vandelay_match_set($match_set_id) or
+        return $e->die_event;
+
+    $e->allowed("ADMIN_IMPORT_MATCH_SET", $set->owner) or
+        return $e->die_event;
+
+    my $tree = $e->search_vandelay_match_set_point([
+        {"match_set" => $match_set_id, "parent" => undef},
+        {"flesh" => -1, "flesh_fields" => {"vmsp" => ["children"]}}
+    ]) or return $e->die_event;
+
+    return pop @$tree;
+}
+
+
+__PACKAGE__->register_method(
+    api_name    => "open-ils.vandelay.match_set.update",
+    method      => "match_set_update_tree",
+    api_level   => 1,
+    argc        => 3,
+    signature   => {
+        desc => q/Replace any vmsp objects associated with a given (by ID) vms
+                with the given objects (recursively fleshed vmsp tree)./
+    }
+);
+
+sub _walk_new_vmsp {
+    my ($e, $match_set_id, $node, $parent_id) = @_;
+
+    my $point = new Fieldmapper::vandelay::match_set_point;
+    $point->parent($parent_id);
+    $point->match_set($match_set_id);
+    $point->$_($node->$_) for (qw/bool_op svf tag subfield negate quality/);
+
+    $e->create_vandelay_match_set_point($point) or return $e->die_event;
+
+    $parent_id = $e->data->id;
+    if ($node->children && @{$node->children}) {
+        for (@{$node->children}) {
+            return $e->die_event if
+                _walk_new_vmsp($e, $match_set_id, $_, $parent_id);
+        }
+    }
+
+    return;
+}
+
+sub match_set_update_tree {
+    my ($self, $conn, $authtoken, $match_set_id, $tree) = @_;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $authtoken);
+    $e->checkauth or return $e->die_event;
+
+    my $set = $e->retrieve_vandelay_match_set($match_set_id) or
+        return $e->die_event;
+
+    $e->allowed("ADMIN_IMPORT_MATCH_SET", $set->owner) or
+        return $e->die_event;
+
+    my $existing = $e->search_vandelay_match_set_point([
+        {"match_set" => $match_set_id},
+        {"order_by" => {"vmsp" => "id DESC"}}
+    ]) or return $e->die_event;
+
+    # delete points, working up from leaf points to the root
+    while(@$existing) {
+        for my $point (shift @$existing) {
+            if( grep {$_->parent eq $point->id} @$existing) {
+                push(@$existing, $point);
+            } else {
+                $e->delete_vandelay_match_set_point($point) or return $e->die_event;
+            }
+        }
+    }
+
+    _walk_new_vmsp($e, $match_set_id, $tree);
+
+    $e->commit or return $e->die_event;
+}
 
 1;
index 2bf6340..6af37fa 100644 (file)
@@ -15,6 +15,8 @@ $$ LANGUAGE plpgsql;
 
 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
 
+CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
+
 CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
     return lc(shift);
 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
index 7841170..fb30850 100644 (file)
@@ -4,12 +4,54 @@ BEGIN;
 
 CREATE SCHEMA vandelay;
 
+CREATE TABLE vandelay.match_set (
+    id      SERIAL  PRIMARY KEY,
+    name    TEXT        NOT NULL,
+    owner   INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE,
+    mtype   TEXT        NOT NULL DEFAULT 'biblio', -- 'biblio','authority','mfhd'?, others?
+    CONSTRAINT name_once_per_owner_mtype UNIQUE (name, owner, mtype)
+);
+
+-- Table to define match points, either FF via SVF or tag+subfield
+CREATE TABLE vandelay.match_set_point (
+    id          SERIAL  PRIMARY KEY,
+    match_set   INT     REFERENCES vandelay.match_set (id),
+    parent      INT     REFERENCES vandelay.match_set_point (id),
+    bool_op     TEXT    CHECK (bool_op IS NULL OR (bool_op IN ('AND','OR','NOT'))),
+    svf         TEXT    REFERENCES config.record_attr_definition (name),
+    tag         TEXT,
+    subfield    TEXT,
+    negate      BOOL    DEFAULT FALSE,
+    quality     INT     NOT NULL DEFAULT 1, -- higher is better
+    CONSTRAINT vmsp_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
+    CONSTRAINT vmsp_need_a_tag_or_a_ff_or_a_bo CHECK (
+        (tag IS NOT NULL AND svf IS NULL AND bool_op IS NULL) OR
+        (tag IS NULL AND svf IS NOT NULL AND bool_op IS NULL) OR
+        (tag IS NULL AND svf IS NULL AND bool_op IS NOT NULL)
+    )
+);
+
+CREATE TABLE vandelay.match_set_quality (
+    id          SERIAL  PRIMARY KEY,
+    match_set   INT     NOT NULL REFERENCES vandelay.match_set (id),
+    svf         TEXT    REFERENCES config.record_attr_definition,
+    tag         TEXT,
+    subfield    TEXT,
+    value       TEXT    NOT NULL,
+    quality     INT     NOT NULL DEFAULT 1, -- higher is better
+    CONSTRAINT vmsq_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
+    CONSTRAINT vmsq_need_a_tag_or_a_ff CHECK ((tag IS NOT NULL AND svf IS NULL) OR (tag IS NULL AND svf IS NOT NULL))
+);
+CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''), value);
+
+
 CREATE TABLE vandelay.queue (
        id                              BIGSERIAL       PRIMARY KEY,
        owner                   INT                     NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
        name                    TEXT            NOT NULL,
        complete                BOOL            NOT NULL DEFAULT FALSE,
        queue_type              TEXT            NOT NULL DEFAULT 'bib' CHECK (queue_type IN ('bib','authority')),
+    match_set       INT         REFERENCES vandelay.match_set (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
        CONSTRAINT vand_queue_name_once_per_owner_const UNIQUE (owner,name,queue_type)
 );
 
@@ -18,7 +60,8 @@ CREATE TABLE vandelay.queued_record (
     create_time        TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
     import_time        TIMESTAMP WITH TIME ZONE,
        purpose         TEXT                                            NOT NULL DEFAULT 'import' CHECK (purpose IN ('import','overlay')),
-    marc               TEXT                        NOT NULL
+    marc               TEXT                        NOT NULL,
+    quality     INT                         NOT NULL DEFAULT 0
 );
 
 
@@ -31,8 +74,7 @@ CREATE TABLE vandelay.bib_attr_definition (
        code            TEXT    UNIQUE NOT NULL,
        description     TEXT,
        xpath           TEXT    NOT NULL,
-       remove          TEXT    NOT NULL DEFAULT '',
-       ident           BOOL    NOT NULL DEFAULT FALSE
+       remove          TEXT    NOT NULL DEFAULT ''
 );
 
 -- Each TEXT field (other than 'name') should hold an XPath predicate for pulling the data needed
@@ -67,6 +109,11 @@ CREATE TABLE vandelay.import_item_attr_definition (
        CONSTRAINT vand_import_item_attr_def_idx UNIQUE (owner,name)
 );
 
+CREATE TABLE vandelay.import_error (
+    code        TEXT    PRIMARY KEY,
+    description TEXT    NOT NULL -- i18n
+);
+
 CREATE TABLE vandelay.bib_queue (
        queue_type          TEXT        NOT NULL DEFAULT 'bib' CHECK (queue_type = 'bib'),
        item_attr_def   BIGINT REFERENCES vandelay.import_item_attr_definition (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
@@ -75,9 +122,11 @@ CREATE TABLE vandelay.bib_queue (
 ALTER TABLE vandelay.bib_queue ADD PRIMARY KEY (id);
 
 CREATE TABLE vandelay.queued_bib_record (
-       queue           INT             NOT NULL REFERENCES vandelay.bib_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-       bib_source      INT             REFERENCES config.bib_source (id) DEFERRABLE INITIALLY DEFERRED,
-       imported_as     BIGINT  REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED
+       queue               INT         NOT NULL REFERENCES vandelay.bib_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+       bib_source          INT         REFERENCES config.bib_source (id) DEFERRABLE INITIALLY DEFERRED,
+       imported_as     BIGINT  REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
+       import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+       error_detail    TEXT
 ) INHERITS (vandelay.queued_record);
 ALTER TABLE vandelay.queued_bib_record ADD PRIMARY KEY (id);
 CREATE INDEX queued_bib_record_queue_idx ON vandelay.queued_bib_record (queue);
@@ -92,17 +141,18 @@ CREATE INDEX queued_bib_record_attr_record_idx ON vandelay.queued_bib_record_att
 
 CREATE TABLE vandelay.bib_match (
        id                              BIGSERIAL       PRIMARY KEY,
-       field_type              TEXT            NOT NULL CHECK (field_type in ('isbn','tcn_value','id')),
-       matched_attr    INT                     REFERENCES vandelay.queued_bib_record_attr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
        queued_record   BIGINT          REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-       eg_record               BIGINT          REFERENCES biblio.record_entry (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+       eg_record               BIGINT          REFERENCES biblio.record_entry (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    quality         INT         NOT NULL DEFAULT 1,
+    match_score     INT         NOT NULL DEFAULT 0
 );
 
--- DROP TABLE vandelay.import_item CASCADE;
 CREATE TABLE vandelay.import_item (
     id              BIGSERIAL   PRIMARY KEY,
     record          BIGINT      NOT NULL REFERENCES vandelay.queued_bib_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     definition      BIGINT      NOT NULL REFERENCES vandelay.import_item_attr_definition (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+       import_error    TEXT        REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+       error_detail    TEXT,
     owning_lib      INT,
     circ_lib        INT,
     call_number     TEXT,
@@ -139,10 +189,583 @@ CREATE TABLE vandelay.merge_profile (
     replace_spec    TEXT,
     strip_spec      TEXT,
     preserve_spec   TEXT,
+    lwm_ratio       NUMERIC,
        CONSTRAINT vand_merge_prof_owner_name_idx UNIQUE (owner,name),
        CONSTRAINT add_replace_strip_or_preserve CHECK ((preserve_spec IS NOT NULL OR replace_spec IS NOT NULL) OR (preserve_spec IS NULL AND replace_spec IS NULL))
 );
 
+CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
+DECLARE
+    ldr         TEXT;
+    tval        TEXT;
+    tval_rec    RECORD;
+    bval        TEXT;
+    bval_rec    RECORD;
+    retval      config.marc21_rec_type_map%ROWTYPE;
+BEGIN
+    ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
+
+    IF ldr IS NULL OR ldr = '' THEN
+        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
+        RETURN retval;
+    END IF;
+
+    SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
+    SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
+
+
+    tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
+    bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
+
+    -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
+
+    SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
+
+
+    IF retval.code IS NULL THEN
+        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
+    END IF;
+
+    RETURN retval;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
+DECLARE
+    rtype       TEXT;
+    ff_pos      RECORD;
+    tag_data    RECORD;
+    val         TEXT;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
+        IF ff_pos.tag = 'ldr' THEN
+            val := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF val IS NOT NULL THEN
+                val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN val;
+            END IF;
+        ELSE
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN val;
+            END LOOP;
+        END IF;
+        val := REPEAT( ff_pos.default_val, ff_pos.length );
+        RETURN val;
+    END LOOP;
+
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
+DECLARE
+    tag_data    TEXT;
+    rtype       TEXT;
+    ff_pos      RECORD;
+    output      biblio.record_ff_map%ROWTYPE;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
+        output.ff_name  := ff_pos.fixed_field;
+        output.ff_value := NULL;
+
+        IF ff_pos.tag = 'ldr' THEN
+            output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF output.ff_value IS NOT NULL THEN
+                output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN NEXT output;
+                output.ff_value := NULL;
+            END IF;
+        ELSE
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
+                IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
+                RETURN NEXT output;
+                output.ff_value := NULL;
+            END LOOP;
+        END IF;
+    
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
+CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
+DECLARE
+    rowid   INT := 0;
+    _007    TEXT;
+    ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
+    psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
+    pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
+    retval  biblio.marc21_physical_characteristics%ROWTYPE;
+BEGIN
+
+    _007 := oils_xpath_string( '//*[@tag="007"]', marc );
+
+    IF _007 IS NOT NULL AND _007 <> '' THEN
+        SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
+
+        IF ptype.ptype_key IS NOT NULL THEN
+            FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
+                SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
+
+                IF pval.id IS NOT NULL THEN
+                    rowid := rowid + 1;
+                    retval.id := rowid;
+                    retval.ptype := ptype.ptype_key;
+                    retval.subfield := psf.id;
+                    retval.value := pval.id;
+                    RETURN NEXT retval;
+                END IF;
+
+            END LOOP;
+        END IF;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE TYPE vandelay.flat_marc AS ( tag CHAR(3), ind1 TEXT, ind2 TEXT, subfield TEXT, value TEXT );
+CREATE OR REPLACE FUNCTION vandelay.flay_marc ( TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+use strict;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+    if ($f->is_control_field) {
+        return_next({ tag => $f->tag, value => $f->data });
+    } else {
+        for my $s ($f->subfields) {
+            return_next({
+                tag      => $f->tag,
+                ind1     => $f->indicator(1),
+                ind2     => $f->indicator(2),
+                subfield => $s->[0],
+                value    => $s->[1]
+            });
+
+            if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
+                my $trim = $f->indicator(2) || 0;
+                return_next({
+                    tag      => 'tnf',
+                    ind1     => $f->indicator(1),
+                    ind2     => $f->indicator(2),
+                    subfield => 'a',
+                    value    => substr( $s->[1], $trim )
+                });
+            }
+        }
+    }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
+DECLARE
+    output  vandelay.flat_marc%ROWTYPE;
+    field   RECORD;
+BEGIN
+    FOR field IN SELECT * FROM vandelay.flay_marc( marc ) LOOP
+        output.ind1 := field.ind1;
+        output.ind2 := field.ind2;
+        output.tag := field.tag;
+        output.subfield := field.subfield;
+        IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
+            output.value := naco_normalize(field.value, field.subfield);
+        ELSE
+            output.value := field.value;
+        END IF;
+
+        CONTINUE WHEN output.value IS NULL;
+
+        RETURN NEXT output;
+    END LOOP;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
+DECLARE
+    transformed_xml TEXT;
+    prev_xfrm       TEXT;
+    normalizer      RECORD;
+    xfrm            config.xml_transform%ROWTYPE;
+    attr_value      TEXT;
+    new_attrs       HSTORE := ''::HSTORE;
+    attr_def        config.record_attr_definition%ROWTYPE;
+BEGIN
+
+    FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
+
+        IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
+            SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(x.value), COALESCE(attr_def.joiner,' ')) INTO attr_value
+              FROM  vandelay.flatten_marc(xml) AS x
+              WHERE x.tag LIKE attr_def.tag
+                    AND CASE
+                        WHEN attr_def.sf_list IS NOT NULL
+                            THEN POSITION(x.subfield IN attr_def.sf_list) > 0
+                        ELSE TRUE
+                        END
+              GROUP BY x.tag
+              ORDER BY x.tag
+              LIMIT 1;
+
+        ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
+            attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
+
+        ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
+
+            SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
+
+            -- See if we can skip the XSLT ... it's expensive
+            IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
+                -- Can't skip the transform
+                IF xfrm.xslt <> '---' THEN
+                    transformed_xml := oils_xslt_process(xml,xfrm.xslt);
+                ELSE
+                    transformed_xml := xml;
+                END IF;
+
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            IF xfrm.name IS NULL THEN
+                -- just grab the marcxml (empty) transform
+                SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
+                prev_xfrm := xfrm.name;
+            END IF;
+
+            attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
+
+        ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
+            SELECT  value::TEXT INTO attr_value
+              FROM  vandelay.marc21_physical_characteristics(xml)
+              WHERE subfield = attr_def.phys_char_sf
+              LIMIT 1; -- Just in case ...
+
+        END IF;
+
+        -- apply index normalizers to attr_value
+        FOR normalizer IN
+            SELECT  n.func AS func,
+                    n.param_count AS param_count,
+                    m.params AS params
+              FROM  config.index_normalizer n
+                    JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
+              WHERE attr = attr_def.name
+              ORDER BY m.pos LOOP
+                EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                    quote_literal( attr_value ) ||
+                    CASE
+                        WHEN normalizer.param_count > 0
+                            THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                            ELSE ''
+                        END ||
+                    ')' INTO attr_value;
+
+        END LOOP;
+
+        -- Add the new value to the hstore
+        new_attrs := new_attrs || hstore( attr_def.name, attr_value );
+
+    END LOOP;
+
+    RETURN new_attrs;
+END;
+$_$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
+    SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_ACCUM(name) FROM config.record_attr_definition));
+$_$ LANGUAGE SQL;
+
+-- Everything between this comment and the beginning of the definition of
+-- vandelay.match_bib_record() is strictly in service of that function.
+CREATE TYPE vandelay.match_set_test_result AS (record BIGINT, quality INTEGER);
+
+CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
+    match_set_id INTEGER, record_xml TEXT
+) RETURNS SETOF vandelay.match_set_test_result AS $$
+DECLARE
+    tags_rstore HSTORE;
+    svf_rstore  HSTORE;
+    coal        TEXT;
+    joins       TEXT;
+    query_      TEXT;
+    wq          TEXT;
+    qvalue      INTEGER;
+    rec         RECORD;
+BEGIN
+    tags_rstore := vandelay.flatten_marc_hstore(record_xml);
+    svf_rstore := vandelay.extract_rec_attrs(record_xml);
+
+    CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
+    CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
+
+    -- generate the where clause and return that directly (into wq), and as
+    -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
+    wq := vandelay.get_expr_from_match_set(match_set_id);
+
+    query_ := 'SELECT bre.id AS record, ';
+
+    -- qrows table is for the quality bits we add to the SELECT clause
+    SELECT ARRAY_TO_STRING(
+        ARRAY_ACCUM('COALESCE(n' || q::TEXT || '.quality, 0)'), ' + '
+    ) INTO coal FROM _vandelay_tmp_qrows;
+
+    -- our query string so far is the SELECT clause and the inital FROM.
+    -- no JOINs yet nor the WHERE clause
+    query_ := query_ || coal || ' AS quality ' || E'\n' ||
+        'FROM biblio.record_entry bre ';
+
+    -- jrows table is for the joins we must make (and the real text conditions)
+    SELECT ARRAY_TO_STRING(ARRAY_ACCUM(j), E'\n') INTO joins
+        FROM _vandelay_tmp_jrows;
+
+    -- add those joins and the where clause to our query.
+    query_ := query_ || joins || E'\n' || 'WHERE ' || wq || ' AND not bre.deleted';
+
+    -- this will return rows of record,quality
+    FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
+        RETURN NEXT rec;
+    END LOOP;
+
+    DROP TABLE _vandelay_tmp_qrows;
+    DROP TABLE _vandelay_tmp_jrows;
+    RETURN;
+END;
+
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
+    record_xml TEXT
+) RETURNS HSTORE AS $$
+BEGIN
+    RETURN (SELECT
+        HSTORE(
+            ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
+            ARRAY_ACCUM(value)
+        )
+        FROM (
+            SELECT tag, subfield, ARRAY_ACCUM(value)::TEXT AS value
+                FROM vandelay.flatten_marc(record_xml)
+                GROUP BY tag, subfield ORDER BY tag, subfield
+        ) subquery
+    );
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
+    match_set_id INTEGER
+) RETURNS TEXT AS $$
+DECLARE
+    root    vandelay.match_set_point;
+BEGIN
+    SELECT * INTO root FROM vandelay.match_set_point
+        WHERE parent IS NULL AND match_set = match_set_id;
+
+    RETURN vandelay.get_expr_from_match_set_point(root);
+END;
+$$  LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
+    node vandelay.match_set_point
+) RETURNS TEXT AS $$
+DECLARE
+    q           TEXT;
+    i           INTEGER;
+    this_op     TEXT;
+    children    INTEGER[];
+    child       vandelay.match_set_point;
+BEGIN
+    SELECT ARRAY_ACCUM(id) INTO children FROM vandelay.match_set_point
+        WHERE parent = node.id;
+
+    IF ARRAY_LENGTH(children, 1) > 0 THEN
+        this_op := vandelay._get_expr_render_one(node);
+        q := '(';
+        i := 1;
+        WHILE children[i] IS NOT NULL LOOP
+            SELECT * INTO child FROM vandelay.match_set_point
+                WHERE id = children[i];
+            IF i > 1 THEN
+                q := q || ' ' || this_op || ' ';
+            END IF;
+            i := i + 1;
+            q := q || vandelay.get_expr_from_match_set_point(child);
+        END LOOP;
+        q := q || ')';
+        RETURN q;
+    ELSIF node.bool_op IS NULL THEN
+        PERFORM vandelay._get_expr_push_qrow(node);
+        PERFORM vandelay._get_expr_push_jrow(node);
+        RETURN vandelay._get_expr_render_one(node);
+    ELSE
+        RETURN '';
+    END IF;
+END;
+$$  LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay._get_expr_push_qrow(
+    node vandelay.match_set_point
+) RETURNS VOID AS $$
+DECLARE
+BEGIN
+    INSERT INTO _vandelay_tmp_qrows (q) VALUES (node.id);
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
+    node vandelay.match_set_point
+) RETURNS VOID AS $$
+DECLARE
+    jrow        TEXT;
+    my_alias    TEXT;
+    op          TEXT;
+    tagkey      TEXT;
+BEGIN
+    IF node.negate THEN
+        op := '<>';
+    ELSE
+        op := '=';
+    END IF;
+
+    IF node.tag IS NOT NULL THEN
+        tagkey := node.tag;
+        IF node.subfield IS NOT NULL THEN
+            tagkey := tagkey || node.subfield;
+        END IF;
+    END IF;
+
+    my_alias := 'n' || node.id::TEXT;
+
+    jrow := 'LEFT JOIN (SELECT *, ' || node.quality ||
+        ' AS quality FROM metabib.';
+    IF node.tag IS NOT NULL THEN
+        jrow := jrow || 'full_rec) ' || my_alias || ' ON (' ||
+            my_alias || '.record = bre.id AND ' || my_alias || '.tag = ''' ||
+            node.tag || '''';
+        IF node.subfield IS NOT NULL THEN
+            jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
+                node.subfield || '''';
+        END IF;
+        jrow := jrow || ' AND (' || my_alias || '.value ' || op ||
+            ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
+    ELSE    -- svf
+        jrow := jrow || 'record_attr) ' || my_alias || ' ON (' ||
+            my_alias || '.id = bre.id AND (' ||
+            my_alias || '.attrs->''' || node.svf ||
+            ''' ' || op || ' $2->''' || node.svf || '''))';
+    END IF;
+    INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay._get_expr_render_one(
+    node vandelay.match_set_point
+) RETURNS TEXT AS $$
+DECLARE
+    s           TEXT;
+BEGIN
+    IF node.bool_op IS NOT NULL THEN
+        RETURN node.bool_op;
+    ELSE
+        RETURN '(n' || node.id::TEXT || '.id IS NOT NULL)';
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.match_bib_record() RETURNS TRIGGER AS $func$
+DECLARE
+    incoming_existing_id    TEXT;
+    my_bib_queue            vandelay.bib_queue%ROWTYPE;
+    test_result             vandelay.match_set_test_result%ROWTYPE;
+    tmp_rec                 BIGINT;
+BEGIN
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
+    DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
+
+    SELECT * INTO my_bib_queue FROM vandelay.bib_queue WHERE id = NEW.queue;
+
+    IF my_bib_queue.match_set IS NULL THEN
+        RETURN NEW;
+    END IF;
+
+    NEW.quality := vandelay.measure_record_quality( NEW.marc, my_bib_queue.match_set );
+
+    -- Perfect matches on 901$c exit early with a match with high quality.
+    incoming_existing_id :=
+        oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]', NEW.marc);
+
+    IF incoming_existing_id IS NOT NULL AND incoming_existing_id != '' THEN
+        SELECT id INTO tmp_rec FROM biblio.record_entry WHERE id = incoming_existing_id::bigint;
+        IF tmp_rec IS NOT NULL THEN
+            INSERT INTO vandelay.bib_match (queued_record, eg_record, quality) VALUES ( NEW.id, incoming_existing_id::bigint, 9999);
+            RETURN NEW;
+        END IF;
+    END IF;
+
+
+    FOR test_result IN SELECT * FROM
+        vandelay.match_set_test_marcxml(my_bib_queue.match_set, NEW.marc) LOOP
+
+        INSERT INTO vandelay.bib_match ( queued_record, eg_record, match_score, quality )
+       SELECT  NEW.id,
+               test_result.record,
+                test_result.quality,
+               vandelay.measure_record_quality( b.marc, my_bib_queue.match_set )
+         FROM  biblio.record_entry b
+         WHERE id = test_result.record;
+
+    END LOOP;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.measure_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
+DECLARE
+    out_q   INT := 0;
+    rvalue  TEXT;
+    test    vandelay.match_set_quality%ROWTYPE;
+BEGIN
+
+    FOR test IN SELECT * FROM vandelay.match_set_quality WHERE match_set = match_set_id LOOP
+        IF test.tag IS NOT NULL THEN
+            FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
+                IF test.value = rvalue THEN
+                    out_q := out_q + test.quality;
+                END IF;
+            END LOOP;
+        ELSE
+            IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) -> test.svf THEN
+                out_q := out_q + test.quality;
+            END IF;
+        END IF;
+    END LOOP;
+
+    RETURN out_q;
+END;
+$_$ LANGUAGE PLPGSQL;
 
 CREATE TYPE vandelay.tcn_data AS (tcn TEXT, tcn_source TEXT, used BOOL);
 CREATE OR REPLACE FUNCTION vandelay.find_bib_tcn_data ( xml TEXT ) RETURNS SETOF vandelay.tcn_data AS $_$
@@ -643,7 +1266,6 @@ DECLARE
     eg_marc         TEXT;
     v_marc          TEXT;
     replace_rule    TEXT;
-    match_count     INT;
 BEGIN
 
     SELECT  q.marc INTO v_marc
@@ -686,11 +1308,50 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL;
 
+CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
+DECLARE
+    eg_id           BIGINT;
+    lwm_ratio_value NUMERIC;
+BEGIN
+
+    lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
+
+    PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
+
+    IF FOUND THEN
+        -- RAISE NOTICE 'already imported, cannot auto-overlay'
+        RETURN FALSE;
+    END IF;
+
+    SELECT  m.eg_record INTO eg_id
+      FROM  vandelay.bib_match m
+            JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
+            JOIN vandelay.bib_queue q ON (qr.queue = q.id)
+            JOIN biblio.record_entry r ON (r.id = m.eg_record)
+      WHERE m.queued_record = import_id
+            AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
+      ORDER BY  m.match_score DESC, -- required match score
+                qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
+                m.id -- when in doubt, use the first match
+      LIMIT 1;
+
+    IF eg_id IS NULL THEN
+        -- RAISE NOTICE 'incoming record is not of high enough quality';
+        RETURN FALSE;
+    END IF;
+
+    RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
+    SELECT vandelay.auto_overlay_bib_record_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
+$$ LANGUAGE SQL;
+
 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record ( import_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
 DECLARE
     eg_id           BIGINT;
     match_count     INT;
-    match_attr      vandelay.bib_attr_definition%ROWTYPE;
 BEGIN
 
     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
@@ -707,23 +1368,15 @@ BEGIN
         RETURN FALSE;
     END IF;
 
-    SELECT  d.* INTO match_attr
-      FROM  vandelay.bib_attr_definition d
-            JOIN vandelay.queued_bib_record_attr a ON (a.field = d.id)
-            JOIN vandelay.bib_match m ON (m.matched_attr = a.id)
-      WHERE m.queued_record = import_id;
-
-    IF NOT (match_attr.xpath ~ '@tag="901"' AND match_attr.xpath ~ '@code="c"') THEN
-        -- RAISE NOTICE 'not a 901c match: %', match_attr.xpath;
-        RETURN FALSE;
-    END IF;
-
+    -- Check that the one match is on the first 901c
     SELECT  m.eg_record INTO eg_id
-      FROM  vandelay.bib_match m
-      WHERE m.queued_record = import_id
-      LIMIT 1;
+      FROM  vandelay.queued_bib_record q
+            JOIN vandelay.bib_match m ON (m.queued_record = q.id)
+      WHERE q.id = import_id
+            AND m.eg_record = oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]',marc)::BIGINT;
 
-    IF eg_id IS NULL THEN
+    IF NOT FOUND THEN
+        -- RAISE NOTICE 'not a 901c match';
         RETURN FALSE;
     END IF;
 
@@ -749,6 +1402,28 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL;
 
+CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( queue_id BIGINT, merge_profile_id INT, lwm_ratio_value NUMERIC ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    queued_record   vandelay.queued_bib_record%ROWTYPE;
+BEGIN
+
+    FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
+
+        IF vandelay.auto_overlay_bib_record_with_best( queued_record.id, merge_profile_id, lwm_ratio_value ) THEN
+            RETURN NEXT queued_record.id;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+    
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
+    SELECT vandelay.auto_overlay_bib_queue_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
+$$ LANGUAGE SQL;
+
 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue ( queue_id BIGINT ) RETURNS SETOF BIGINT AS $$
     SELECT * FROM vandelay.auto_overlay_bib_queue( $1, NULL );
 $$ LANGUAGE SQL;
@@ -1022,6 +1697,10 @@ DECLARE
     atype   TEXT;
     adef    RECORD;
 BEGIN
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
     FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
 
         SELECT extract_marc_field('vandelay.queued_bib_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_bib_record WHERE id = NEW.id;
@@ -1041,6 +1720,10 @@ DECLARE
     item_data   vandelay.import_item%ROWTYPE;
 BEGIN
 
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
 
     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
@@ -1095,88 +1778,12 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
-DECLARE
-    attr        RECORD;
-    attr_def    RECORD;
-    eg_rec      RECORD;
-    id_value    TEXT;
-    exact_id    BIGINT;
+CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
 BEGIN
-
-    DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
-
-    SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
-
-    IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
-        id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
-    
-        IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
-            SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
-            SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
-            IF exact_id IS NOT NULL THEN
-                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
-            END IF;
-        END IF;
-    END IF;
-
-    IF exact_id IS NULL THEN
-        FOR attr IN SELECT a.* FROM vandelay.queued_bib_record_attr a JOIN vandelay.bib_attr_definition d ON (d.id = a.field) WHERE record = NEW.id AND d.ident IS TRUE LOOP
-    
-               -- All numbers? check for an id match
-               IF (attr.attr_value ~ $r$^\d+$$r$) THEN
-               FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
-                       INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
-                       END LOOP;
-               END IF;
-    
-               -- Looks like an ISBN? check for an isbn match
-               IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
-               FOR eg_rec IN EXECUTE $$SELECT * FROM metabib.full_rec fr WHERE fr.value LIKE evergreen.lowercase('$$ || attr.attr_value || $$%') AND fr.tag = '020' AND fr.subfield = 'a'$$ LOOP
-                               PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
-                               IF FOUND THEN
-                               INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
-                               END IF;
-                       END LOOP;
-    
-                       -- subcheck for isbn-as-tcn
-                   FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
-                           INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
-               END LOOP;
-               END IF;
-    
-               -- check for an OCLC tcn_value match
-               IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
-                   FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
-                           INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
-               END LOOP;
-               END IF;
-    
-               -- check for a direct tcn_value match
-            FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
-                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
-            END LOOP;
-    
-               -- check for a direct item barcode match
-            FOR eg_rec IN
-                    SELECT  DISTINCT b.*
-                      FROM  biblio.record_entry b
-                            JOIN asset.call_number cn ON (cn.record = b.id)
-                            JOIN asset.copy cp ON (cp.call_number = cn.id)
-                      WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
-            LOOP
-                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
-            END LOOP;
-    
-        END LOOP;
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
     END IF;
 
-    RETURN NULL;
-END;
-$func$ LANGUAGE PLPGSQL;
-
-CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
-BEGIN
     DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
     DELETE FROM vandelay.import_item WHERE record = OLD.id;
 
@@ -1200,7 +1807,7 @@ CREATE TRIGGER ingest_item_trigger
     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_bib_items();
 
 CREATE TRIGGER zz_match_bibs_trigger
-    AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
+    BEFORE INSERT OR UPDATE ON vandelay.queued_bib_record
     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_bib_record();
 
 
@@ -1211,8 +1818,7 @@ CREATE TABLE vandelay.authority_attr_definition (
        code            TEXT    UNIQUE NOT NULL,
        description     TEXT,
        xpath           TEXT    NOT NULL,
-       remove          TEXT    NOT NULL DEFAULT '',
-       ident           BOOL    NOT NULL DEFAULT FALSE
+       remove          TEXT    NOT NULL DEFAULT ''
 );
 
 CREATE TABLE vandelay.authority_queue (
@@ -1223,7 +1829,9 @@ ALTER TABLE vandelay.authority_queue ADD PRIMARY KEY (id);
 
 CREATE TABLE vandelay.queued_authority_record (
        queue           INT     NOT NULL REFERENCES vandelay.authority_queue (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-       imported_as     INT     REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED
+       imported_as     INT     REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
+       import_error    TEXT    REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+       error_detail    TEXT
 ) INHERITS (vandelay.queued_record);
 ALTER TABLE vandelay.queued_authority_record ADD PRIMARY KEY (id);
 CREATE INDEX queued_authority_record_queue_idx ON vandelay.queued_authority_record (queue);
@@ -1238,9 +1846,9 @@ CREATE INDEX queued_authority_record_attr_record_idx ON vandelay.queued_authorit
 
 CREATE TABLE vandelay.authority_match (
        id                              BIGSERIAL       PRIMARY KEY,
-       matched_attr    INT                     REFERENCES vandelay.queued_authority_record_attr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
        queued_record   BIGINT          REFERENCES vandelay.queued_authority_record (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
-       eg_record               BIGINT          REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED
+       eg_record               BIGINT          REFERENCES authority.record_entry (id) DEFERRABLE INITIALLY DEFERRED,
+    quality         INT         NOT NULL DEFAULT 0
 );
 
 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
@@ -1249,6 +1857,10 @@ DECLARE
     atype   TEXT;
     adef    RECORD;
 BEGIN
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
     FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
 
         SELECT extract_marc_field('vandelay.queued_authority_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_authority_record WHERE id = NEW.id;
@@ -1264,6 +1876,10 @@ $$ LANGUAGE PLPGSQL;
 
 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
 BEGIN
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
     DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
     IF TG_OP = 'UPDATE' THEN
         RETURN NEW;
index 5dc4761..1d4016b 100644 (file)
@@ -372,6 +372,27 @@ CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS
        SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
 $func$ LANGUAGE SQL;
 
+CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
+DECLARE
+       auth    authority.record_entry%ROWTYPE;
+       output  authority.full_rec%ROWTYPE;
+       field   RECORD;
+BEGIN
+       SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
+
+       FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
+               output.record := rid;
+               output.ind1 := field.ind1;
+               output.ind2 := field.ind2;
+               output.tag := field.tag;
+               output.subfield := field.subfield;
+               output.value := field.value;
+
+               RETURN NEXT output;
+       END LOOP;
+END;
+$func$ LANGUAGE PLPGSQL;
+
 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
 DECLARE
        bib     biblio.record_entry%ROWTYPE;
@@ -380,19 +401,13 @@ DECLARE
 BEGIN
        SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
 
-       FOR field IN SELECT * FROM biblio.flatten_marc( bib.marc ) LOOP
+       FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
                output.record := rid;
                output.ind1 := field.ind1;
                output.ind2 := field.ind2;
                output.tag := field.tag;
                output.subfield := field.subfield;
-               IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
-                       output.value := naco_normalize(field.value, field.subfield);
-               ELSE
-                       output.value := field.value;
-               END IF;
-
-               CONTINUE WHEN output.value IS NULL;
+               output.value := field.value;
 
                RETURN NEXT output;
        END LOOP;
@@ -426,200 +441,19 @@ $func$ LANGUAGE PLPGSQL;
 --             )x(record int, tag text, ind1 text, ind2 text, subfield text, value text);
 -- $func$ LANGUAGE SQL;
 
-CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use MARC::Charset;
-
-MARC::Charset->assume_unicode(1);
-
-my $xml = shift;
-my $r = MARC::Record->new_from_xml( $xml );
-
-return_next( { tag => 'LDR', value => $r->leader } );
-
-for my $f ( $r->fields ) {
-       if ($f->is_control_field) {
-               return_next({ tag => $f->tag, value => $f->data });
-       } else {
-               for my $s ($f->subfields) {
-                       return_next({
-                               tag      => $f->tag,
-                               ind1     => $f->indicator(1),
-                               ind2     => $f->indicator(2),
-                               subfield => $s->[0],
-                               value    => $s->[1]
-                       });
-
-                       if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
-                               my $trim = $f->indicator(2) || 0;
-                               return_next({
-                                       tag      => 'tnf',
-                                       ind1     => $f->indicator(1),
-                                       ind2     => $f->indicator(2),
-                                       subfield => 'a',
-                                       value    => substr( $s->[1], $trim )
-                               });
-                       }
-               }
-       }
-}
-
-return undef;
-
-$func$ LANGUAGE PLPERLU;
-
-CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
-DECLARE
-       ldr         TEXT;
-       tval        TEXT;
-       tval_rec    RECORD;
-       bval        TEXT;
-       bval_rec    RECORD;
-    retval      config.marc21_rec_type_map%ROWTYPE;
-BEGIN
-    ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
-
-    IF ldr IS NULL OR ldr = '' THEN
-        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
-        RETURN retval;
-    END IF;
-
-    SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
-    SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
-
-
-    tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
-    bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
-
-    -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
-
-    SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
-
-
-    IF retval.code IS NULL THEN
-        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
-    END IF;
-
-    RETURN retval;
-END;
-$func$ LANGUAGE PLPGSQL;
 
 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
 $func$ LANGUAGE SQL;
 
-CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
-DECLARE
-    rtype       TEXT;
-    ff_pos      RECORD;
-    tag_data    RECORD;
-    val         TEXT;
-BEGIN
-    rtype := (vandelay.marc21_record_type( marc )).code;
-    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
-        IF ff_pos.tag = 'ldr' THEN
-            val := oils_xpath_string('//*[local-name()="leader"]', marc);
-            IF val IS NOT NULL THEN
-                val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
-                RETURN val;
-            END IF;
-        ELSE
-            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
-                val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
-                RETURN val;
-            END LOOP;
-        END IF;
-        val := REPEAT( ff_pos.default_val, ff_pos.length );
-        RETURN val;
-    END LOOP;
-
-    RETURN NULL;
-END;
-$func$ LANGUAGE PLPGSQL;
-
 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
 $func$ LANGUAGE SQL;
 
-CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
-CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
-DECLARE
-    tag_data    TEXT;
-    rtype       TEXT;
-    ff_pos      RECORD;
-    output      biblio.record_ff_map%ROWTYPE;
-BEGIN
-    rtype := (vandelay.marc21_record_type( marc )).code;
-
-    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
-        output.ff_name  := ff_pos.fixed_field;
-        output.ff_value := NULL;
-
-        IF ff_pos.tag = 'ldr' THEN
-            output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
-            IF output.ff_value IS NOT NULL THEN
-                output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
-                RETURN NEXT output;
-                output.ff_value := NULL;
-            END IF;
-        ELSE
-            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
-                output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
-                IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
-                RETURN NEXT output;
-                output.ff_value := NULL;
-            END LOOP;
-        END IF;
-
-    END LOOP;
-
-    RETURN;
-END;
-$func$ LANGUAGE PLPGSQL;
-
 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
 $func$ LANGUAGE SQL;
 
-CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
-CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
-DECLARE
-    rowid   INT := 0;
-    _007    TEXT;
-    ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
-    psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
-    pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
-    retval  biblio.marc21_physical_characteristics%ROWTYPE;
-BEGIN
-
-    _007 := oils_xpath_string( '//*[@tag="007"]', marc );
-
-    IF _007 IS NOT NULL AND _007 <> '' THEN
-        SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
-
-        IF ptype.ptype_key IS NOT NULL THEN
-            FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
-                SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
-
-                IF pval.id IS NOT NULL THEN
-                    rowid := rowid + 1;
-                    retval.id := rowid;
-                    retval.ptype := ptype.ptype_key;
-                    retval.subfield := psf.id;
-                    retval.value := pval.id;
-                    RETURN NEXT retval;
-                END IF;
-
-            END LOOP;
-        END IF;
-    END IF;
-
-    RETURN;
-END;
-$func$ LANGUAGE PLPGSQL;
-
 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
 $func$ LANGUAGE SQL;
@@ -1024,87 +858,12 @@ BEGIN
         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
         IF NOT FOUND THEN
-            FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
-
-                IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
-                    SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
-                      FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
-                      WHERE record = NEW.id
-                            AND tag LIKE attr_def.tag
-                            AND CASE
-                                WHEN attr_def.sf_list IS NOT NULL 
-                                    THEN POSITION(subfield IN attr_def.sf_list) > 0
-                                ELSE TRUE
-                                END
-                      GROUP BY tag
-                      ORDER BY tag
-                      LIMIT 1;
-
-                ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
-                    attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
-
-                ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
-
-                    SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
-            
-                    -- See if we can skip the XSLT ... it's expensive
-                    IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
-                        -- Can't skip the transform
-                        IF xfrm.xslt <> '---' THEN
-                            transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
-                        ELSE
-                            transformed_xml := NEW.marc;
-                        END IF;
-            
-                        prev_xfrm := xfrm.name;
-                    END IF;
-
-                    IF xfrm.name IS NULL THEN
-                        -- just grab the marcxml (empty) transform
-                        SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
-                        prev_xfrm := xfrm.name;
-                    END IF;
-
-                    attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
-
-                ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
-                    SELECT  m.value INTO attr_value
-                      FROM  biblio.marc21_physical_characteristics(NEW.id) v
-                            JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
-                      WHERE v.subfield = attr_def.phys_char_sf
-                      LIMIT 1; -- Just in case ...
-
-                END IF;
-
-                -- apply index normalizers to attr_value
-                FOR normalizer IN
-                    SELECT  n.func AS func,
-                            n.param_count AS param_count,
-                            m.params AS params
-                      FROM  config.index_normalizer n
-                            JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
-                      WHERE attr = attr_def.name
-                      ORDER BY m.pos LOOP
-                        EXECUTE 'SELECT ' || normalizer.func || '(' ||
-                            quote_literal( attr_value ) ||
-                            CASE
-                                WHEN normalizer.param_count > 0
-                                    THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
-                                    ELSE ''
-                                END ||
-                            ')' INTO attr_value;
-        
-                END LOOP;
-
-                -- Add the new value to the hstore
-                new_attrs := new_attrs || hstore( attr_def.name, attr_value );
-
-            END LOOP;
+            new_attrs := vandelay.extract_rec_attrs(NEW.marc);
 
             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
             ELSE
-                UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
+                UPDATE metabib.record_attr SET attrs = new_attrs WHERE id = NEW.id;
             END IF;
 
         END IF;
index 1a2259c..6610af1 100644 (file)
@@ -2486,13 +2486,13 @@ INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (2, 'author', oils_i18n_gettext(2, 'Author of work', 'vqbrad', 'description'),'//*[@tag="100" or @tag="110" or @tag="113"]/*[contains("ad",@code)]');
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (3, 'language', oils_i18n_gettext(3, 'Language of work', 'vqbrad', 'description'),'//*[@tag="240"]/*[@code="l"][1]');
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (4, 'pagination', oils_i18n_gettext(4, 'Pagination', 'vqbrad', 'description'),'//*[@tag="300"]/*[@code="a"][1]');
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident, remove ) VALUES (5, 'isbn',oils_i18n_gettext(5, 'ISBN', 'vqbrad', 'description'),'//*[@tag="020"]/*[@code="a"]', TRUE, $r$(?:-|\s.+$)$r$);
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident, remove ) VALUES (6, 'issn',oils_i18n_gettext(6, 'ISSN', 'vqbrad', 'description'),'//*[@tag="022"]/*[@code="a"]', TRUE, $r$(?:-|\s.+$)$r$);
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, remove ) VALUES (5, 'isbn',oils_i18n_gettext(5, 'ISBN', 'vqbrad', 'description'),'//*[@tag="020"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, remove ) VALUES (6, 'issn',oils_i18n_gettext(6, 'ISSN', 'vqbrad', 'description'),'//*[@tag="022"]/*[@code="a"]', $r$(?:-|\s.+$)$r$);
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (7, 'price',oils_i18n_gettext(7, 'Price', 'vqbrad', 'description'),'//*[@tag="020" or @tag="022"]/*[@code="c"][1]');
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (8, 'rec_identifier',oils_i18n_gettext(8, 'Accession Number', 'vqbrad', 'description'),'//*[@tag="001"]', TRUE);
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (9, 'eg_tcn',oils_i18n_gettext(9, 'TCN Value', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="a"]', TRUE);
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (10, 'eg_tcn_source',oils_i18n_gettext(10, 'TCN Source', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="b"]', TRUE);
-INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, ident ) VALUES (11, 'eg_identifier',oils_i18n_gettext(11, 'Internal ID', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="c"]', TRUE);
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath) VALUES (8, 'rec_identifier',oils_i18n_gettext(8, 'Accession Number', 'vqbrad', 'description'),'//*[@tag="001"]');
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath) VALUES (9, 'eg_tcn',oils_i18n_gettext(9, 'TCN Value', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="a"]');
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath) VALUES (10, 'eg_tcn_source',oils_i18n_gettext(10, 'TCN Source', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="b"]');
+INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath) VALUES (11, 'eg_identifier',oils_i18n_gettext(11, 'Internal ID', 'vqbrad', 'description'),'//*[@tag="901"]/*[@code="c"]');
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (12, 'publisher',oils_i18n_gettext(12, 'Publisher', 'vqbrad', 'description'),'//*[@tag="260"]/*[@code="b"][1]');
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath, remove ) VALUES (13, 'pubdate',oils_i18n_gettext(13, 'Publication Date', 'vqbrad', 'description'),'//*[@tag="260"]/*[@code="c"][1]',$r$\D$r$);
 INSERT INTO vandelay.bib_attr_definition ( id, code, description, xpath ) VALUES (14, 'edition',oils_i18n_gettext(14, 'Edition', 'vqbrad', 'description'),'//*[@tag="250"]/*[@code="a"][1]');
@@ -2546,7 +2546,7 @@ INSERT INTO vandelay.import_item_attr_definition (
     'k'
 );
 
-INSERT INTO vandelay.authority_attr_definition (id, code, description, xpath, ident ) VALUES (1, 'rec_identifier',oils_i18n_gettext(1, 'Identifier', 'vqarad', 'description'),'//*[@tag="001"]', TRUE);
+INSERT INTO vandelay.authority_attr_definition (id, code, description, xpath) VALUES (1, 'rec_identifier',oils_i18n_gettext(1, 'Identifier', 'vqarad', 'description'),'//*[@tag="001"]');
 SELECT SETVAL('vandelay.authority_attr_definition_id_seq'::TEXT, 100);
 
 
@@ -7956,6 +7956,18 @@ INSERT INTO action_trigger.environment (event_def, path) VALUES
     (37, 'circ_lib.billing_address')
 ;
 
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'general.unknown', oils_i18n_gettext('general.unknown', 'Import or Overlay failed', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.duplicate.barcode', oils_i18n_gettext('import.item.duplicate.barcode', 'Import failed due to barcode collision', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.circ_modifier', oils_i18n_gettext('import.item.invalid.circ_modifier', 'Import failed due to invalid circulation modifier', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.location', oils_i18n_gettext('import.item.invalid.location', 'Import failed due to invalid copy location', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.sysid', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.tcn', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.missing.sysid', oils_i18n_gettext('overlay.missing.sysid', 'Overlay failed due to missing system id', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.auth.duplicate.acn', oils_i18n_gettext('import.auth.duplicate.acn', 'Import failed due to Accession Number collision', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.xml.malformed', oils_i18n_gettext('import.xml.malformed', 'Malformed record cause Import failure', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.xml.malformed', oils_i18n_gettext('overlay.xml.malformed', 'Malformed record cause Overlay failure', 'vie', 'description') );
+INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.record.quality', oils_i18n_gettext('overlay.record.quality', 'New record had insufficient quality', 'vie', 'description') );
+
 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
     'ui.cat.volume_copy_editor.horizontal',
     oils_i18n_gettext(
index 4cfa20c..8e2aaa5 100644 (file)
@@ -3,8 +3,6 @@ DROP SCHEMA IF EXISTS unapi CASCADE;
 BEGIN;
 CREATE SCHEMA unapi;
 
-CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
-
 CREATE TABLE unapi.bre_output_layout (
     name                TEXT    PRIMARY KEY,
     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
index eeb7284..19371df 100644 (file)
@@ -1367,67 +1367,6 @@ CREATE OR REPLACE FUNCTION authority.propagate_changes (aid BIGINT) RETURNS SETO
     SELECT authority.propagate_changes( authority, bib ) FROM authority.bib_linking WHERE authority = $1;
 $func$ LANGUAGE SQL;
 
-CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use MARC::Charset;
-
-MARC::Charset->assume_unicode(1);
-
-my $xml = shift;
-my $r = MARC::Record->new_from_xml( $xml );
-
-return_next( { tag => 'LDR', value => $r->leader } );
-
-for my $f ( $r->fields ) {
-    if ($f->is_control_field) {
-        return_next({ tag => $f->tag, value => $f->data });
-    } else {
-        for my $s ($f->subfields) {
-            return_next({
-                tag      => $f->tag,
-                ind1     => $f->indicator(1),
-                ind2     => $f->indicator(2),
-                subfield => $s->[0],
-                value    => $s->[1]
-            });
-
-        }
-    }
-}
-
-return undef;
-
-$func$ LANGUAGE PLPERLU;
-
-CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
-DECLARE
-    auth    authority.record_entry%ROWTYPE;
-    output    authority.full_rec%ROWTYPE;
-    field    RECORD;
-BEGIN
-    SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
-
-    FOR field IN SELECT * FROM authority.flatten_marc( auth.marc ) LOOP
-        output.record := rid;
-        output.ind1 := field.ind1;
-        output.ind2 := field.ind2;
-        output.tag := field.tag;
-        output.subfield := field.subfield;
-        IF field.subfield IS NOT NULL THEN
-            output.value := naco_normalize(field.value, field.subfield);
-        ELSE
-            output.value := field.value;
-        END IF;
-
-        CONTINUE WHEN output.value IS NULL;
-
-        RETURN NEXT output;
-    END LOOP;
-END;
-$func$ LANGUAGE PLPGSQL;
-
 -- authority.rec_descriptor appears to be unused currently
 CREATE OR REPLACE FUNCTION authority.reingest_authority_rec_descriptor( auth_id BIGINT ) RETURNS VOID AS $func$
 BEGIN
diff --git a/Open-ILS/src/sql/Pg/upgrade/renumber_me.sql b/Open-ILS/src/sql/Pg/upgrade/renumber_me.sql
new file mode 100644 (file)
index 0000000..92340b1
--- /dev/null
@@ -0,0 +1,456 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('test'); -- phasefx
+
+INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
+        'vandelay.queued_bib_record.print',
+        'vqbr', 
+        oils_i18n_gettext(
+            'vandelay.queued_bib_record.print',
+            'Print output has been requested for records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.queued_bib_record.csv',
+        'vqbr', 
+        oils_i18n_gettext(
+            'vandelay.queued_bib_record.csv',
+            'CSV output has been requested for records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.queued_bib_record.email',
+        'vqbr', 
+        oils_i18n_gettext(
+            'vandelay.queued_bib_record.email',
+            'An email has been requested for records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.queued_auth_record.print',
+        'vqar', 
+        oils_i18n_gettext(
+            'vandelay.queued_auth_record.print',
+            'Print output has been requested for records in an Importer Authority Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.queued_auth_record.csv',
+        'vqar', 
+        oils_i18n_gettext(
+            'vandelay.queued_auth_record.csv',
+            'CSV output has been requested for records in an Importer Authority Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.queued_auth_record.email',
+        'vqar', 
+        oils_i18n_gettext(
+            'vandelay.queued_auth_record.email',
+            'An email has been requested for records in an Importer Authority Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.import_items.print',
+        'vii', 
+        oils_i18n_gettext(
+            'vandelay.import_items.print',
+            'Print output has been requested for Import Items from records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.import_items.csv',
+        'vii', 
+        oils_i18n_gettext(
+            'vandelay.import_items.csv',
+            'CSV output has been requested for Import Items from records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+    ,(
+        'vandelay.import_items.email',
+        'vii', 
+        oils_i18n_gettext(
+            'vandelay.import_items.email',
+            'An email has been requested for Import Items from records in an Importer Bib Queue.',
+            'ath',
+            'description'
+        ), 
+        FALSE
+    )
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        38,
+        TRUE,
+        1,
+        'Print Output for Queued Bib Records',
+        'vandelay.queued_bib_record.print',
+        'NOOP_True',
+        'ProcessTemplate',
+        'queue.owner',
+        'print-on-demand',
+$$
+[%- USE date -%]
+<pre>
+Queue ID: [% target.0.queue.id %]
+Queue Name: [% target.0.queue.name %]
+Queue Type: [% target.0.queue.queue_type %]
+Complete? [% target.0.queue.complete %]
+
+    [% FOR vqbr IN target %]
+=-=-=
+ Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
+ Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
+ Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
+ Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
+ ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
+ ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
+ Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
+ Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
+ TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
+ TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
+ Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
+ Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
+ Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
+ Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
+ Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
+
+    [% END %]
+</pre>
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    38, 'attributes')
+    ,( 38, 'queue')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        39,
+        TRUE,
+        1,
+        'CSV Output for Queued Bib Records',
+        'vandelay.queued_bib_record.csv',
+        'NOOP_True',
+        'ProcessTemplate',
+        'queue.owner',
+        'print-on-demand',
+$$
+[%- USE date -%][%- FOR vqbr IN target -%]"[% helpers.get_queued_bib_attr('title',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('author',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('language',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pagination',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('issn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('price',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('publisher',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('edition',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) | replace('"', '""') %]"[%- END -%]
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    39, 'attributes')
+    ,( 39, 'queue')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        40,
+        TRUE,
+        1,
+        'Email Output for Queued Bib Records',
+        'vandelay.queued_bib_record.email',
+        'NOOP_True',
+        'SendEmail',
+        'queue.owner',
+        NULL,
+$$
+[%- USE date -%]
+[%- SET user = target.0.queue.owner -%]
+To: [%- params.recipient_email || user.email || 'root@localhost' %]
+From: [%- params.sender_email || default_sender %]
+Subject: Bibs from Import Queue
+
+Queue ID: [% target.0.queue.id %]
+Queue Name: [% target.0.queue.name %]
+Queue Type: [% target.0.queue.queue_type %]
+Complete? [% target.0.queue.complete %]
+
+    [% FOR vqbr IN target %]
+=-=-=
+ Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
+ Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
+ Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
+ Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
+ ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
+ ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
+ Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
+ Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
+ TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
+ TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
+ Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
+ Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
+ Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
+ Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
+ Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
+
+    [% END %]
+
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    40, 'attributes')
+    ,( 40, 'queue')
+    ,( 40, 'queue.owner')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        41,
+        TRUE,
+        1,
+        'Print Output for Queued Authority Records',
+        'vandelay.queued_auth_record.print',
+        'NOOP_True',
+        'ProcessTemplate',
+        'queue.owner',
+        'print-on-demand',
+$$
+[%- USE date -%]
+<pre>
+Queue ID: [% target.0.queue.id %]
+Queue Name: [% target.0.queue.name %]
+Queue Type: [% target.0.queue.queue_type %]
+Complete? [% target.0.queue.complete %]
+
+    [% FOR vqar IN target %]
+=-=-=
+ Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
+
+    [% END %]
+</pre>
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    41, 'attributes')
+    ,( 41, 'queue')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        42,
+        TRUE,
+        1,
+        'CSV Output for Queued Authority Records',
+        'vandelay.queued_auth_record.csv',
+        'NOOP_True',
+        'ProcessTemplate',
+        'queue.owner',
+        'print-on-demand',
+$$
+[%- USE date -%][%- FOR vqar IN target -%]"[% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) | replace('"', '""') %]"[%- END -%]
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    42, 'attributes')
+    ,( 42, 'queue')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        43,
+        TRUE,
+        1,
+        'Email Output for Queued Authority Records',
+        'vandelay.queued_auth_record.email',
+        'NOOP_True',
+        'SendEmail',
+        'queue.owner',
+        NULL,
+$$
+[%- USE date -%]
+[%- SET user = target.0.queue.owner -%]
+To: [%- params.recipient_email || user.email || 'root@localhost' %]
+From: [%- params.sender_email || default_sender %]
+Subject: Authorities from Import Queue
+
+Queue ID: [% target.0.queue.id %]
+Queue Name: [% target.0.queue.name %]
+Queue Type: [% target.0.queue.queue_type %]
+Complete? [% target.0.queue.complete %]
+
+    [% FOR vqar IN target %]
+=-=-=
+ Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
+
+    [% END %]
+
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    43, 'attributes')
+    ,( 43, 'queue')
+    ,( 43, 'queue.owner')
+;
+
+INSERT INTO action_trigger.event_definition (
+        id,
+        active,
+        owner,
+        name,
+        hook,
+        validator,
+        reactor,
+        group_field,
+        granularity,
+        template
+    ) VALUES (
+        44,
+        TRUE,
+        1,
+        'Print Output for Import Items from Queued Bib Records',
+        'vandelay.import_items.print',
+        'NOOP_True',
+        'ProcessTemplate',
+        'record.queue.owner',
+        'print-on-demand',
+$$
+[%- USE date -%]
+<pre>
+Queue ID: [% target.0.record.queue.id %]
+Queue Name: [% target.0.record.queue.name %]
+Queue Type: [% target.0.record.queue.queue_type %]
+Complete? [% target.0.record.queue.complete %]
+
+    [% FOR vii IN target %]
+=-=-=
+ Import Item ID         | [% vii.id %]
+ Title of work          | [% helpers.get_queued_bib_attr('title',vii.record.attributes) %]
+ ISBN                   | [% helpers.get_queued_bib_attr('isbn',vii.record.attributes) %]
+ Attribute Definition   | [% vii.definition %]
+ Import Error           | [% vii.import_error %]
+ Import Error Detail    | [% vii.error_detail %]
+ Owning Library         | [% vii.owning_lib %]
+ Circulating Library    | [% vii.circ_lib %]
+ Call Number            | [% vii.call_number %]
+ Copy Number            | [% vii.copy_number %]
+ Status                 | [% vii.status.name %]
+ Shelving Location      | [% vii.location.name %]
+ Circulate              | [% vii.circulate %]
+ Deposit                | [% vii.deposit %]
+ Deposit Amount         | [% vii.deposit_amount %]
+ Reference              | [% vii.ref %]
+ Holdable               | [% vii.holdable %]
+ Price                  | [% vii.price %]
+ Barcode                | [% vii.barcode %]
+ Circulation Modifier   | [% vii.circ_modifier %]
+ Circulate As MARC Type | [% vii.circ_as_type %]
+ Alert Message          | [% vii.alert_message %]
+ Public Note            | [% vii.pub_note %]
+ Private Note           | [% vii.priv_note %]
+ OPAC Visible           | [% vii.opac_visible %]
+
+    [% END %]
+</pre>
+$$
+    )
+;
+
+INSERT INTO action_trigger.environment ( event_def, path) VALUES (
+    44, 'record')
+    ,( 44, 'record.attributes')
+    ,( 44, 'record.queue')
+    ,( 44, 'record.queue.owner')
+;
+
+COMMIT;
+
+-- BEGIN; DELETE FROM action_trigger.event_output WHERE id IN ((SELECT template_output FROM action_trigger.event WHERE event_def IN (38,39,40,41,42,43,44))UNION(SELECT error_output FROM action_trigger.event WHERE event_def IN (38,39,40,41,42,43,44))); DELETE FROM action_trigger.event WHERE event_def IN (38,39,40,41,42,43,44); DELETE FROM action_trigger.environment WHERE event_def IN (38,39,40,41,42,43,44); DELETE FROM action_trigger.event_definition WHERE id IN (38,39,40,41,42,43,44); DELETE FROM action_trigger.hook WHERE key IN ('vandelay.queued_bib_record.print','vandelay.queued_bib_record.csv','vandelay.queued_bib_record.email','vandelay.queued_auth_record.print','vandelay.queued_auth_record.csv','vandelay.queued_auth_record.email','vandelay.import_items.print','vandelay.import_items.csv','vandelay.import_items.email'); DELETE FROM config.upgrade_log WHERE version = 'test'; COMMIT;
index d60ba06..b1987f8 100644 (file)
@@ -28,3 +28,23 @@ table.dijitTooltipTable { border-collapse: separate; }
 .export_tr_border td { border-top: 1px solid #808080; }
 .nav_row_div {padding:1px; text-align:center; }
 .toolbar_selected { border: 2px dashed #808080; text-decoration:underline; font-weight:bold;}
+
+#vl-import-error-record { margin: 10px; }
+#vl-import-error-record tr:nth-child(even) {background: #EEE}
+#vl-import-error-record td:first-child { text-decoration: underline; }
+#vl-import-error-record td { padding: 5px; }
+#vl-error-import-error { font-weight: bold; }
+
+#vl-queue-filter-fieldset { padding-top: 3px; border:2px dashed #d9e8f9; }
+#vl-queue-filter-fieldset { margin: 5px; }
+#vl-queue-filter-fieldset td { padding: 5px; }
+#vl-queue-filter-fieldset th { padding: 5px; }
+#vl-queue-filter-fieldset legend { font-weight: bold; font-size: 1.3em }
+
+#vl-queue-filter-table { margin-left: 15px; padding-left: 15px; border-left: 2px dashed #d9e8f9;}
+#vl-queue-summary-table { margin-left: 15px; padding-left: 15px; border-left: 2px dashed #d9e8f9;}
+.queue-nav-table tr:nth-child(even) {background: #EEE}
+.queue-nav-table-label { font-weight: bold; text-decoration:underline;}
+.queue-pager-span { padding-right: 5px; margin-right: 5px; border-right: 2px solid #e8e1cf; }
+#vl-queue-paging-table td { padding-bottom: 0px; }
+#vl-file-label { margin-right: 10px; }
index bb369c6..df8b73b 100644 (file)
@@ -87,6 +87,14 @@ if(!dojo._hasResource["fieldmapper.Fieldmapper"]){
                 return true;
             }
             return;
+        },
+
+        toString : function() {
+            /* ever so slightly aid debugging */
+            if (this.classname)
+                return "[object fieldmapper." + this.classname + "]";
+            else
+                return Object.prototype.toString();
         }
 
     });
diff --git a/Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js b/Open-ILS/web/js/dojo/openils/vandelay/TreeDndSource.js
new file mode 100644 (file)
index 0000000..7dd9fa5
--- /dev/null
@@ -0,0 +1,123 @@
+dojo.provide("openils.vandelay.TreeDndSource");
+dojo.require("dijit._tree.dndSource");
+
+/* This class specifically serves the eg/vandelay/match_set interface
+ * for editing Vandelay Match Set trees.  It should probably  have a more
+ * specific name that reflects that.
+ */
+dojo.declare(
+    "openils.vandelay.TreeDndSource", dijit._tree.dndSource, {
+        "_is_replaceable": function(spoint, dpoint, disroot) {
+            /* An OP can replace anything, but non-OPs can only replace other
+             * non-OPs.
+             */
+            if (spoint.bool_op())
+                return true;
+            else if (!dpoint.bool_op())
+                return true;
+            return false;
+        },
+        "constructor": function() {
+            /* Given a tree object, there seems to be no way to access its
+             * dndController, which seems to be the only thing that knows
+             * about a tree's selected nodes.  So we register instances
+             * in a global variable in order to find them later. :-(
+             */
+            if (!window._tree_dnd_controllers)
+                window._tree_dnd_controllers = [];
+
+            window._tree_dnd_controllers.push(this);
+
+            dojo.connect(
+                this.tree.model.store, "onNew", this,
+                function() { this.redraw_expression_preview(); }
+            );
+            dojo.connect(
+                this.tree.model.store, "onDelete", this,
+                function() { this.redraw_expression_preview(); }
+            );
+        },
+        "redraw_expression_preview": function() {
+            if (typeof(window.redraw_expression_preview) == "function") {
+                window.redraw_expression_preview();
+            } else {
+                console.log("no redraw_expression_preview function registered");
+            }
+        },
+        "checkItemAcceptance": function(target, source, position) {
+            if (!source._ready || source == this) return;
+
+            if (this.tree.model.replace_mode) {
+                var ditem = dijit.getEnclosingWidget(target).item;
+                return (
+                    position == "over" && this._is_replaceable(
+                        source.getAllNodes()[0].match_point,
+                        this.tree.model.store.getValue(ditem, "match_point"),
+                        ditem === this.tree.model.root
+                    )
+                );
+            } else {
+                return (
+                    position != "over" ||
+                    this.tree.model.mayHaveChildren(
+                        dijit.getEnclosingWidget(target).item
+                    )
+                );
+            }
+            /* code in match_set.js makes sure that source._ready gets set true
+             * only when we want the item to be draggable */
+        },
+        "itemCreator": function(nodes, somethingelse) {
+            var default_items = this.inherited(arguments);
+            for (var i = 0; i < default_items.length; i++)
+                default_items[i].match_point = nodes[i].match_point;
+            return default_items;
+        },
+        "onDndDrop": function(source, nodes, copy) {
+            if (
+                !this.tree.model.replace_mode ||
+                this.containerState != "Over" ||
+                this.dropPosition == "Before" ||
+                this.dropPosition == "After" ||
+                source == this
+            ) {
+                return this.inherited(arguments);
+            }
+
+            /* This method only comes into play for our "replace mode" */
+
+            var target_widget = dijit.getEnclosingWidget(this.targetAnchor);
+            var new_params = this.itemCreator(nodes, this.targetAnchor)[0];
+
+            /* Here, we morph target_widget.item into the new item */
+
+            var store = this.tree.model.store;
+            var item = target_widget.item;
+            for (var k in new_params) {
+                if (k == "id") continue;    /* can't use this / don't need it */
+                store.setValue(item, k, new_params[k]);
+            }
+            if (this.tree.model.root === item) {    /* replacing root node */
+                if (!new_params.match_point.bool_op()) {
+                    /* If we're here, we've replaced the root node with
+                     * something that isn't a bool op, so we need to nuke
+                     * any children that the item has.
+                     */
+                    store.setValue(item, "children", []);
+                }
+            }
+            if (typeof(window.render_vmsp_label) == "function") {
+                store.setValue(
+                    item,
+                    "name",
+                    window.render_vmsp_label(new_params.match_point)
+                );
+            }
+
+            this.redraw_expression_preview();
+
+            /* just because this is at the end of the default implementation: */
+            this.onDndCancel();
+        }
+    }
+);
diff --git a/Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js b/Open-ILS/web/js/dojo/openils/vandelay/TreeStoreModel.js
new file mode 100644 (file)
index 0000000..e694238
--- /dev/null
@@ -0,0 +1,52 @@
+dojo.provide("openils.vandelay.TreeStoreModel");
+dojo.require("dijit.tree.TreeStoreModel");
+dojo.require("openils.Util");
+
+/* This class specifically serves the eg/vandelay/match_set interface
+ * for editing Vandelay Match Set trees.  It should probably  have a more
+ * specific name that reflects that.
+ */
+
+function _simple_item(model, item) {
+    /* Instead of model.getLabel(), could do
+     * model.store.getValue(item, "blah") or something like that ... */
+    var mp = model.store.getValue(item, "match_point");
+    mp.children([]);
+    return mp;
+}
+
+dojo.declare(
+    "openils.vandelay.TreeStoreModel", dijit.tree.TreeStoreModel, {
+        "replace_mode": 0,
+        "get_simple_tree": function(item, oncomplete, result) {
+            var self = this;
+            var me;
+            if (!result) {
+                me = result = _simple_item(this, item);
+            } else {
+                me = _simple_item(this, item);
+                result.push(me);
+            }
+
+            if (this.mayHaveChildren(item)) {
+                this.getChildren(
+                    item, function(children) {
+                        var kids_here = [];
+                        for (var i = 0; i < children.length; i++) {
+                            self.get_simple_tree(children[i], null, kids_here);
+                        }
+                        me.children(kids_here);
+                        if (oncomplete) oncomplete(result);
+                    }
+                );
+            }
+        },
+        "mayHaveChildren": function(item) {
+            var match_point = this.store.getValue(item, "match_point");
+            if (match_point)
+                return openils.Util.isTrue(match_point.bool_op());
+            else
+                return true;
+        }
+    }
+);
diff --git a/Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js b/Open-ILS/web/js/dojo/openils/vandelay/nls/match_set.js
new file mode 100644 (file)
index 0000000..92851d2
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "DEFINE_MP": "Define this match point using the above fields, then drag me to the tree on the right.",
+    "LEAVE_ROOT_ALONE": "You cannot delete the root node of a tree (but you can replace it).",
+    "EXACTLY_ONE": "First select exactly one node from the tree on the right side of the screen.",
+    "EXIT_REPLACE_MODE": "Exit Replace Mode",
+    "ENTER_REPLACE_MODE": "Enter Replace Mode",
+    "NO_CAN_DO": "An error has occurred. Close this interface and try again.",
+    "OK": "Ok",
+    "CANCEL": "Cancel",
+    "POINT_NEEDS_ONE": "You have not entered valid data",
+    "FAULTY_MARC": "A MARC tag must be identified by three digits, and the subfield must be one non-whitespace, non-control character.",
+    "WORKING_MP_HERE": "Choose from among the three buttons above to add a new match point.",
+    "WORKING_QM_HERE": "Use buttons above and to the right to add new quality metrics.",
+    "SVF": "Record Attribute"
+}
index ecbdf95..9e5be71 100644 (file)
@@ -640,6 +640,13 @@ if(!dojo._hasResource['openils.widget.AutoGrid']) {
         */
 
         return autoWidget.getDisplayString();
-    }
+    };
+
+    openils.widget.AutoGrid.orgUnitGetter = function(rowIndex, item) {
+        if (!item) return "";
+        return fieldmapper.aou.findOrgUnit(
+            this.grid.store.getValue(item, this.field)
+        ).shortname();
+    };
 }
 
diff --git a/Open-ILS/web/js/ui/default/conify/global/vandelay/match_set.js b/Open-ILS/web/js/ui/default/conify/global/vandelay/match_set.js
new file mode 100644 (file)
index 0000000..28f30b5
--- /dev/null
@@ -0,0 +1,598 @@
+dojo.require("dijit.Tree");
+dojo.require("dijit.form.Button");
+dojo.require("dojo.data.ItemFileWriteStore");
+dojo.require("dojo.dnd.Source");
+dojo.require("openils.vandelay.TreeDndSource");
+dojo.require("openils.vandelay.TreeStoreModel");
+dojo.require("openils.CGI");
+dojo.require("openils.User");
+dojo.require("openils.Util");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.widget.ProgressDialog");
+dojo.require("openils.widget.AutoGrid");
+
+var localeStrings, node_editor, qnode_editor, _crads, CGI, tree, match_set;
+
+var NodeEditorAbstract = {
+    "_svf_select_template": null,
+    "_simple_value_getter": function(control) {
+        if (typeof control.selectedIndex != "undefined")
+            return control.options[control.selectedIndex].value;
+        else if (dojo.attr(control, "type") == "checkbox")
+            return control.checked;
+        else
+            return control.value;
+    },
+    "is_sensible": function(thing) {
+        var need_one = 0;
+        this.foi.forEach(function(field) { if (thing[field]()) need_one++; });
+
+        if (need_one != 1) {
+            alert(localeStrings.POINT_NEEDS_ONE);
+            return false;
+        }
+
+        if (thing.tag()) {
+            if (
+                !thing.tag().match(/^\d{3}$/) ||
+                thing.subfield().length != 1 ||
+                !thing.subfield().match(/\S/) ||
+                thing.subfield().charCodeAt(0) < 32
+            ) {
+                alert(localeStrings.FAULTY_MARC);
+                return false;
+            }
+        }
+
+        return true;
+    },
+    "_add_consistent_controls": function(tgt) {
+        if (!this._consistent_controls) {
+            var trs = dojo.query(this._consistent_controls_query);
+            this._consistent_controls = [];
+            for (var i = 0; i < trs.length; i++)
+                this._consistent_controls[i] = dojo.clone(trs[i]);
+        }
+
+        this._consistent_controls.forEach(
+            function(node) { dojo.place(dojo.clone(node), tgt); }
+        );
+    },
+    "_factories_by_type": {
+        "svf": function() {
+            if (!self._svf_select_template) {
+                self._svf_select_template = dojo.create(
+                    "select", {"fmfield": "svf"}
+                );
+                for (var i=0; i<_crads.length; i++) {
+                    dojo.create(
+                        "option", {
+                            "value": _crads[i].name(),
+                            "innerHTML": _crads[i].label()
+                        }, self._svf_select_template
+                    );
+                }
+            }
+
+            var select = dojo.clone(self._svf_select_template);
+            dojo.attr(select, "id", "svf-select");
+            var label = dojo.create(
+                "label", {
+                    "for": "svf-select", "innerHTML": localeStrings.SVF + ":"
+                }
+            );
+
+            var tr = dojo.create("tr");
+            dojo.place(label, dojo.create("td", null, tr));
+            dojo.place(select, dojo.create("td", null, tr));
+
+            return [tr];
+        },
+        "tag": function() {
+            var rows = [dojo.create("tr"), dojo.create("tr")];
+            dojo.create(
+                "label", {
+                    "for": "tag-input", "innerHTML": "Tag:"
+                }, dojo.create("td", null, rows[0])
+            );
+            dojo.create(
+                "input", {
+                    "id": "tag-input",
+                    "type": "text",
+                    "size": 4,
+                    "maxlength": 3,
+                    "fmfield": "tag"
+                }, dojo.create("td", null, rows[0])
+            );
+            dojo.create(
+                "label", {
+                    "for": "subfield-input", "innerHTML": "Subfield: \u2021"
+                }, dojo.create("td", null, rows[1])
+            );
+            dojo.create(
+                "input", {
+                    "id": "subfield-input",
+                    "type": "text",
+                    "size": 2,
+                    "maxlength": 1,
+                    "fmfield": "subfield"
+                }, dojo.create("td", null, rows[1])
+            );
+            return rows;
+        },
+        "bool_op": function() {
+            var tr = dojo.create("tr");
+            dojo.create(
+                "label",
+                {"for": "operator-select", "innerHTML": "Operator:"},
+                dojo.create("td", null, tr)
+            );
+            var select = dojo.create(
+                "select", {"fmfield": "bool_op", "id": "operator-select"},
+                dojo.create("td", null, tr)
+            );
+            dojo.create("option", {"value": "AND", "innerHTML": "AND"}, select);
+            dojo.create("option", {"value": "OR", "innerHTML": "OR"}, select);
+
+            return [tr];
+        }
+    }
+};
+
+function apply_base_class(cls, basecls) {
+    openils.Util.objectProperties(basecls).forEach(
+        function(m) { cls[m] = basecls[m]; }
+    );
+}
+
+function QualityNodeEditor() {
+    var self = this;
+    this.foi = ["tag", "svf"]; /* Fields of Interest - starting points for UI */
+
+    this._init = function(qnode_editor_container) {
+        this._consistent_controls_query =
+            "[consistent-controls], [quality-controls]";
+        this.qnode_editor_container = dojo.byId(qnode_editor_container);
+        this.clear();
+    };
+
+    this.clear = function() {
+        dojo.create(
+            "em", {"innerHTML": localeStrings.WORKING_QM_HERE},
+            this.qnode_editor_container, "only"
+        );
+    };
+
+    this.build_vmsq = function() {
+        var metric = new vmsq();
+        metric.match_set(match_set.id());   /* using global */
+        var controls = dojo.query("[fmfield]", this.qnode_editor_container);
+        for (var i = 0; i < controls.length; i++) {
+            var field = dojo.attr(controls[i], "fmfield");
+            var value = this._simple_value_getter(controls[i]);
+            metric[field](value);
+        }
+
+        if (!this.is_sensible(metric)) return null;    /* will alert() */
+        else return metric;
+    };
+
+    this.add = function(type) {
+        this.clear();
+
+        /* these are the editing widgets */
+        var table = dojo.create("table", {"className": "node-editor"});
+
+        var nodes = this._factories_by_type[type]();
+        for (var i = 0; i < nodes.length; i++) dojo.place(nodes[i], table);
+
+        this._add_consistent_controls(table);
+
+        var ok_cxl_td = dojo.create(
+            "td", {"colspan": 2, "align": "center", "className": "space-me"},
+            dojo.create("tr", null, table)
+        );
+
+        dojo.create(
+            "input", {
+                "type": "submit", "value": localeStrings.OK,
+                "onclick": function() {
+                    var metric = self.build_vmsq();
+                    if (metric) {
+                        self.clear();
+                        pcrud.create(
+                            metric, {
+                                /* borrowed from openils.widget.AutoGrid */
+                                "oncomplete": function(req, cudResults) {
+                                    var fmObject = cudResults[0];
+                                    if (vmsq_grid.onPostCreate)
+                                        vmsq_grid.onPostCreate(fmObject);
+                                    if (fmObject) {
+                                        vmsq_grid.store.newItem(
+                                            fmObject.toStoreItem()
+                                        );
+                                    }
+                                    setTimeout(function() {
+                                        try {
+                                            vmsq_grid.selection.select(vmsq_grid.rowCount-1);
+                                            vmsq_grid.views.views[0].getCellNode(vmsq_grid.rowCount-1, 1).focus();
+                                        } catch (E) {}
+                                    },200);
+                                }
+                            }
+                        );
+                    }
+                }
+            }, ok_cxl_td
+        );
+        dojo.create(
+            "input", {
+                "type": "reset", "value": localeStrings.CANCEL,
+                "onclick": function() { self.clear(); }
+            }, ok_cxl_td
+        );
+
+        dojo.place(table, this.qnode_editor_container, "only");
+
+        /* nice */
+        try { dojo.query("select, input", table)[0].focus(); }
+        catch(E) { console.log(String(E)); }
+
+    };
+
+    apply_base_class(self, NodeEditorAbstract);
+    this._init.apply(this, arguments);
+}
+
+function NodeEditor() {
+    var self = this;
+    this.foi = ["tag", "svf", "bool_op"]; /* Fields of Interest - starting points for UI */
+
+    this._init = function(dnd_source, node_editor_container) {
+        this._consistent_controls_query =
+            "[consistent-controls], [point-controls]";
+        this.dnd_source = dnd_source;
+        this.node_editor_container = dojo.byId(node_editor_container);
+    };
+
+    this.clear = function() {
+        this.dnd_source.selectAll().deleteSelectedNodes();
+        dojo.create(
+            "em", {"innerHTML": localeStrings.WORKING_MP_HERE},
+            this.node_editor_container, "only"
+        );
+        this.dnd_source._ready = false;
+    };
+
+    this.build_vmsp = function() {
+        var match_point = new vmsp();
+        var controls = dojo.query("[fmfield]", this.node_editor_container);
+        for (var i = 0; i < controls.length; i++) {
+            var field = dojo.attr(controls[i], "fmfield");
+            var value = this._simple_value_getter(controls[i]);
+            match_point[field](value);
+        }
+
+        if (!this.is_sensible(match_point)) return null;    /* will alert() */
+        else return match_point;
+    };
+
+    this.update_draggable = function(draggable) {
+        var mp;
+
+        if (!(mp = this.build_vmsp())) return;  /* will alert() */
+
+        draggable.match_point = mp;
+        dojo.attr(draggable, "innerHTML", render_vmsp_label(mp));
+        this.dnd_source._ready = true;
+    };
+
+    this.add = function(type) {
+        this.clear();
+
+        /* a representation, not the editing widgets, but will also carry
+         * the fieldmapper object when dragged to the tree */
+        var draggable = dojo.create(
+            "li", {"innerHTML": localeStrings.DEFINE_MP}
+        );
+
+        /* these are the editing widgets */
+        var table = dojo.create("table", {"className": "node-editor"});
+
+        var nodes = this._factories_by_type[type]();
+        for (var i = 0; i < nodes.length; i++) dojo.place(nodes[i], table);
+
+        if (type != "bool_op")
+            this._add_consistent_controls(table);
+
+        dojo.create(
+            "input", {
+                "type": "submit", "value": localeStrings.OK,
+                "onclick": function() { self.update_draggable(draggable); }
+            }, dojo.create(
+                "td", {"colspan": 2, "align": "center"},
+                dojo.create("tr", null, table)
+            )
+        );
+
+        dojo.place(table, this.node_editor_container, "only");
+
+        this.dnd_source.insertNodes(false, [draggable]);
+
+        /* nice */
+        try { dojo.query("select, input", table)[0].focus(); }
+        catch(E) { console.log(String(E)); }
+
+    };
+
+    apply_base_class(self, NodeEditorAbstract);
+
+    this._init.apply(this, arguments);
+}
+
+function find_crad_by_name(name) {
+    for (var i = 0; i < _crads.length; i++) {
+        if (_crads[i].name() == name)
+            return _crads[i];
+    }
+    return null;
+}
+
+function render_vmsp_label(point, minimal) {
+    /* "minimal" has this implication:
+     * for svf, only show the code, not the longer label.
+     */
+    if (point.bool_op()) {
+        return point.bool_op();
+    } else if (point.svf()) {
+        return (openils.Util.isTrue(point.negate()) ? "NOT " : "") + (
+            minimal ?  point.svf() :
+                (point.svf() + " / " + find_crad_by_name(point.svf()).label())
+        );
+    } else {
+        return (openils.Util.isTrue(point.negate()) ? "NOT " : "") +
+            point.tag() + " \u2021" + point.subfield();
+    }
+}
+
+function replace_mode(explicit) {
+    if (typeof explicit == "undefined")
+        tree.model.replace_mode ^= 1;
+    else
+        tree.model.replace_mode = explicit;
+
+    dojo.attr(
+        "replacer", "innerHTML",
+        localeStrings[
+            (tree.model.replace_mode ? "EXIT" : "ENTER") + "_REPLACE_MODE"
+        ]
+    );
+    dojo[tree.model.replace_mode ? "addClass" : "removeClass"](
+        "replacer", "replace-mode"
+    );
+}
+
+function delete_selected_in_tree() {
+    /* relies on the fact that we only have one tree that would have
+     * registered a dnd controller. */
+    _tree_dnd_controllers[0].getSelectedItems().forEach(
+        function(item) {
+            if (item === tree.model.root)
+                alert(localeStrings.LEAVE_ROOT_ALONE);
+            else
+                tree.model.store.deleteItem(item);
+        }
+    );
+}
+
+function new_match_set_tree() {
+    var point = new vmsp();
+    point.bool_op("AND");
+    return [
+        {
+            "id": "root",
+            "children": [],
+            "name": render_vmsp_label(point),
+            "match_point": point
+        }
+    ];
+}
+
+/* dojoize_match_set_tree() takes an argument, "point", that is actually a
+ * vmsp fieldmapper object with descendants fleshed hierarchically. It turns
+ * that into a syntactically flat array but preserving the hierarchy
+ * semantically in the language used by dojo data stores, i.e.,
+ *
+ * [
+ *  {'id': 'root', children:[{'_reference': '0'}, {'_reference': '1'}]},
+ *  {'id': '0', children:[]},
+ *  {'id': '1', children:[]}
+ * ],
+ *
+ */
+function dojoize_match_set_tree(point, refgen) {
+    var root = false;
+    if (!refgen) {
+        if (!point) {
+            return new_match_set_tree();
+        }
+        refgen = 0;
+        root = true;
+    }
+
+    var bathwater = point.children();
+    point.children([]);
+    var item = {
+        "id": (root ? "root" : refgen),
+        "name": render_vmsp_label(point),
+        "match_point": point.clone(),
+        "children": []
+    };
+    point.children(bathwater);
+
+    var results = [item];
+
+    if (point.children()) {
+        for (var i = 0; i < point.children().length; i++) {
+            var child = point.children()[i];
+            item.children.push({"_reference": ++refgen});
+            results = results.concat(
+                dojoize_match_set_tree(child, refgen)
+            );
+        }
+    }
+
+    return results;
+}
+
+function render_vms_metadata(match_set) {
+    dojo.byId("vms-name").innerHTML = match_set.name();
+    dojo.byId("vms-owner").innerHTML =
+        aou.findOrgUnit(match_set.owner()).name();
+    dojo.byId("vms-mtype").innerHTML = match_set.mtype();
+}
+
+function redraw_expression_preview() {
+    tree.model.getRoot(
+        function(root) {
+            tree.model.get_simple_tree(
+                root, function(r) {
+                    dojo.attr(
+                        "expr-preview",
+                        "innerHTML",
+                        render_expression_preview(r)
+                    );
+                }
+            );
+        }
+    );
+}
+
+function render_expression_preview(r) {
+    if (r.children().length) {
+        return "(" + r.children().map(render_expression_preview).join(
+            " " + render_vmsp_label(r) + " "
+        ) + ")";
+    } else if (!r.bool_op()) {
+        return render_vmsp_label(r, true /* minimal */);
+    } else {
+        return "()";
+    }
+}
+
+function save_tree() {
+    progress_dialog.show(true);
+
+    tree.model.getRoot(
+        function(root) {
+            tree.model.get_simple_tree(
+                root, function(r) {
+                    fieldmapper.standardRequest(
+                        ["open-ils.vandelay",
+                            "open-ils.vandelay.match_set.update"], {
+                            "params": [
+                                openils.User.authtoken, match_set.id(), r
+                            ],
+                            "async": true,
+                            "oncomplete": function(r) {
+                                progress_dialog.hide();
+                                /* catch exceptions */
+                                r = openils.Util.readResponse(r);
+
+                                location.href = location.href;
+                            }
+                        }
+                    );
+                }
+            );
+        }
+    );
+}
+
+function init_vmsq_grid() {
+    vmsq_grid.loadAll(
+        {"order_by": {"vmsq": "quality"}},
+        {"match_set": match_set.id()}
+    );
+}
+
+function my_init() {
+    progress_dialog.show(true);
+
+    dojo.requireLocalization("openils.vandelay", "match_set");
+    localeStrings = dojo.i18n.getLocalization("openils.vandelay", "match_set");
+
+    pcrud = new openils.PermaCrud();
+    CGI = new openils.CGI();
+
+    if (!CGI.param("match_set")) {
+        alert(localeStrings.NO_CAN_DO);
+        progress_dialog.hide();
+        return;
+    }
+
+    render_vms_metadata(
+        match_set = pcrud.retrieve("vms", CGI.param("match_set"))
+    );
+
+    /* No-one should have hundreds of these or anything, but theoretically
+     * this could be problematic with a big enough list of crad objects. */
+    _crads = pcrud.retrieveAll("crad", {"order_by": {"crad": "label"}});
+
+    var match_set_tree = fieldmapper.standardRequest(
+        ["open-ils.vandelay", "open-ils.vandelay.match_set.get_tree"],
+        [openils.User.authtoken, CGI.param("match_set")]
+    );
+
+    var store = new dojo.data.ItemFileWriteStore({
+        "data": {
+            "identifier": "id",
+            "label": "name",
+            "items": dojoize_match_set_tree(match_set_tree)
+        }
+    });
+
+    var tree_model = new openils.vandelay.TreeStoreModel({
+        "store": store, "query": {"id": "root"}
+    });
+
+    var src = new dojo.dnd.Source("src-here");
+    tree = new dijit.Tree(
+        {
+            "model": tree_model,
+            "dndController": openils.vandelay.TreeDndSource,
+            "dragThreshold": 8,
+            "betweenThreshold": 5,
+            "persist": false
+        }, "tree-here"
+    );
+
+    node_editor = new NodeEditor(src, "node-editor-container");
+    qnode_editor = new QualityNodeEditor("qnode-editor-container");
+
+    replace_mode(0);
+
+    dojo.connect(
+        src, "onDndDrop", null,
+        function(source, nodes, copy, target) {
+            /* Because of the... interesting... characteristics of DnD
+             * design in dojo/dijit (at least as of 1.3), this callback will
+             * fire both for our working node dndSource and for the tree!
+             */
+            if (source == this)
+                node_editor.clear();  /* ... because otherwise this acts like a
+                                         copy operation no matter what the user
+                                         does, even though we really want a
+                                         "move." */
+        }
+    );
+
+    redraw_expression_preview();
+    node_editor.clear();
+
+    init_vmsq_grid();
+
+    progress_dialog.hide();
+}
+
+openils.Util.addOnLoad(my_init);
index a54005f..7c96ed5 100644 (file)
@@ -57,7 +57,8 @@ var globalDivs = [
     'vl-attr-editor-div',
     'vl-marc-export-div',
     'vl-profile-editor-div',
-    'vl-item-attr-editor-div'
+    'vl-item-attr-editor-div',
+    'vl-import-error-div'
 ];
 
 var authtoken;
@@ -85,13 +86,15 @@ var cgi = new openils.CGI();
 var vlQueueGridColumePicker = {};
 var vlBibSources = [];
 var importItemDefs = [];
+var matchSets = {};
+var mergeProfiles = [];
 
 /**
   * Grab initial data
   */
 function vlInit() {
     authtoken = openils.User.authtoken;
-    var initNeeded = 6; // how many async responses do we need before we're init'd 
+    var initNeeded = 7; // how many async responses do we need before we're init'd 
     var initCount = 0; // how many async reponses we've received
 
     openils.Util.registerEnterHandler(
@@ -104,13 +107,13 @@ function vlInit() {
             runStartupCommands();
     }
 
-    var profiles = new openils.PermaCrud().retrieveAll('vmp');
-    vlUploadMergeProfile.store = new dojo.data.ItemFileReadStore({data:fieldmapper.vmp.toStoreData(profiles)});
+    mergeProfiles = new openils.PermaCrud().retrieveAll('vmp');
+    vlUploadMergeProfile.store = new dojo.data.ItemFileReadStore({data:fieldmapper.vmp.toStoreData(mergeProfiles)});
     vlUploadMergeProfile.labelAttr = 'name';
     vlUploadMergeProfile.searchAttr = 'name';
     vlUploadMergeProfile.startup();
 
-    vlUploadMergeProfile2.store = new dojo.data.ItemFileReadStore({data:fieldmapper.vmp.toStoreData(profiles)});
+    vlUploadMergeProfile2.store = new dojo.data.ItemFileReadStore({data:fieldmapper.vmp.toStoreData(mergeProfiles)});
     vlUploadMergeProfile2.labelAttr = 'name';
     vlUploadMergeProfile2.searchAttr = 'name';
     vlUploadMergeProfile2.startup();
@@ -164,7 +167,25 @@ function vlInit() {
         }
     );
 
+    new openils.PermaCrud().search('vms',
+        {owner: owner.map(function(org) { return org.id(); })},
+        {   async: true,
+            oncomplete: function(r) {
+                var sets = openils.Util.readResponse(r);
+                dojo.forEach(sets, 
+                    function(set) {
+                        if(!matchSets[set.mtype()])
+                            matchSets[set.mtype()] = [];
+                        matchSets[set.mtype()].push(set);
+                    }
+                );
+                checkInitDone();
+            }
+        }
+    );
+
     vlAttrEditorInit();
+    vlExportInit();
 }
 
 
@@ -257,6 +278,10 @@ function displayGlobalDiv(id) {
     openils.Util.removeCSSClass(dojo.byId('vl-menu-queue-select'), 'toolbar_selected');
     openils.Util.removeCSSClass(dojo.byId('vl-menu-attr-editor'), 'toolbar_selected');
     openils.Util.removeCSSClass(dojo.byId('vl-menu-profile-editor'), 'toolbar_selected');
+    openils.Util.removeCSSClass(dojo.byId('vl-menu-match-set-editor'), 'toolbar_selected');
+
+    if(dojo.byId('vl-match-set-iframe'))
+        dojo.byId('vl-match-set-editor-div').removeChild(dojo.byId('vl-match-set-iframe'));
 
     switch(id) {
         case 'vl-marc-export-div':
@@ -277,6 +302,9 @@ function displayGlobalDiv(id) {
         case 'vl-item-attr-editor-div':
             openils.Util.addCSSClass(dojo.byId('vl-menu-import-item-attr-editor'), 'toolbar_selected');
             break;
+        case 'vl-match-set-editor-div':
+            openils.Util.addCSSClass(dojo.byId('vl-menu-match-set-editor'), 'toolbar_selected');
+            break;
     }
 }
 
@@ -311,13 +339,13 @@ function uploadMARC(onload){
 /**
   * Creates a new vandelay queue
   */
-function createQueue(queueName, type, onload, importDefId) {
+function createQueue(queueName, type, onload, importDefId, matchSet) {
     var name = (type=='bib') ? 'bib' : 'authority';
     var method = 'open-ils.vandelay.'+ name +'_queue.create'
     fieldmapper.standardRequest(
         ['open-ils.vandelay', method],
         {   async: true,
-            params: [authtoken, queueName, null, name, importDefId],
+            params: [authtoken, queueName, null, name, matchSet, importDefId],
             oncomplete : function(r) {
                 var queue = r.recv().content();
                 if(e = openils.Event.parse(queue)) 
@@ -348,47 +376,112 @@ function processSpool(key, queueId, type, onload) {
     );
 }
 
-function retrieveQueuedRecords(type, queueId, onload) {
+function vlExportInit() {
+
+    // queue export
+    var qsel = dojo.byId('vl-queue-export-options');
+    qsel.onchange = function(newVal) {
+        var value = qsel.options[qsel.selectedIndex].value;
+        qsel.selectedIndex = 0;
+        if(!value) return;
+        if(!confirm('Export as "' + value + '"?')) return; // TODO: i18n
+        retrieveQueuedRecords(
+            currentType, 
+            currentQueueId, 
+            function(r) { 
+                exportHandler(value, r);
+                displayGlobalDiv('vl-queue-div');
+            },
+            value
+        );
+    }
+
+    // item export
+    var isel = dojo.byId('vl-item-export-options');
+    isel.onchange = function(newVal) {
+        var value = isel.options[isel.selectedIndex].value;
+        isel.selectedIndex = 0;
+        if(!value) return;
+        if(!confirm('Export as "' + value + '"?')) return; // TODO: i18n
+
+        var method = 'open-ils.vandelay.import_item.queue.export.' + value;
+
+        fieldmapper.standardRequest(
+            ['open-ils.vandelay', method],
+            {
+                params : [
+                    authtoken, 
+                    currentQueueId, 
+                    {with_import_error: (vlImportItemsShowErrors.checked) ? 1 : null}
+                ],
+                async : true,
+                oncomplete : function(r) {exportHandler(value, r)}
+            }
+        );
+    }
+}
+
+function exportHandler(type, response) {
+    try {
+        var content = openils.Util.readResponse(response);
+        if (type=='email') {
+            if (content) { throw(content); }
+            return;
+        }
+        content = content[0].template_output().data();
+        switch(type) {
+            case 'print':
+                openils.Util.printHtmlString(content);
+            break;
+            case 'csv':
+                //content = content.replace(/\\t/g,'\t'); // if we really wanted to do .tsv instead
+                openils.XUL.contentToFileSaveDialog(content);
+            break;
+            default:
+                alert('response = ' + response + '\tcontent:\n' + content);
+        }
+    } catch(E) {
+        alert('Error exporting data: ' + E);
+    }
+}
+
+function retrieveQueuedRecords(type, queueId, onload, doExport) {
     displayGlobalDiv('vl-generic-progress');
     queuedRecords = [];
     queuedRecordsMap = {};
     currentOverlayRecordsMap = {};
     currentOverlayRecordsMapGid = {};
     selectableGridRecords = {};
-    //resetVlQueueGridLayout();
 
     if(!type) type = currentType;
     if(!queueId) queueId = currentQueueId;
     if(!onload) onload = handleRetrieveRecords;
 
-    var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve.atomic';
+    var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve';
+
+    if(doExport) method += '.export.' + doExport;
     if(vlQueueGridShowMatches.checked)
         method = method.replace('records', 'records.matches');
 
+    method += '.atomic';
+
     var sel = dojo.byId('vl-queue-display-limit-selector');
     var limit = parseInt(sel.options[sel.selectedIndex].value);
     var offset = limit * parseInt(vlQueueDisplayPage.attr('value')-1);
 
-    var params =  [authtoken, queueId, {clear_marc: 1, offset: offset, limit: limit}];
+    var params =  [authtoken, queueId, {clear_marc: 1, offset: offset, limit: limit, flesh_import_items:1}];
     if(vlQueueGridShowNonImport.checked)
         params[2].non_imported = 1;
 
+    if(vlQueueGridShowImportErrors.checked)
+        params[2].with_import_error = 1;
+
     fieldmapper.standardRequest(
         ['open-ils.vandelay', method],
         {   async: true,
             params: params,
-            /*
-            onresponse: function(r) {
-                console.log("ONREPONSE");
-                var rec = r.recv().content();
-                if(e = openils.Event.parse(rec))
-                    return alert(e);
-                console.log("got record " + rec.id());
-                queuedRecords.push(rec);
-                queuedRecordsMap[rec.id()] = rec;
-            },
-            */
             oncomplete: function(r){
+                if(doExport) return onload(r);
                 var recs = r.recv().content();
                 if(e = openils.Event.parse(recs[0]))
                     return alert(e);
@@ -405,7 +498,8 @@ function retrieveQueuedRecords(type, queueId, onload) {
 
 function vlLoadMatchUI(recId) {
     displayGlobalDiv('vl-generic-progress');
-    var matches = queuedRecordsMap[recId].matches();
+    var queuedRec = queuedRecordsMap[recId];
+    var matches = queuedRec.matches();
     var records = [];
     currentImportRecId = recId;
     for(var i = 0; i < matches.length; i++)
@@ -415,7 +509,7 @@ function vlLoadMatchUI(recId) {
     var params = [records];
     if(currentType == 'auth') {
         retrieve = ['open-ils.cat', 'open-ils.cat.authority.record.retrieve'];
-        parmas = [authtoken, records, {clear_marc:1}];
+        params = [authtoken, records, {clear_marc:1}];
     }
 
     fieldmapper.standardRequest(
@@ -435,7 +529,7 @@ function vlLoadMatchUI(recId) {
 
                 // build the data store of records with match information
                 var dataStore = bre.toStoreData(recs, null, 
-                    {virtualFields:['dest_matchpoint', 'src_matchpoint', '_id']});
+                    {virtualFields:['_id', 'match_score', 'match_quality', 'rec_quality']});
                 dataStore.identifier = '_id';
 
                 var matchSeenMap = {};
@@ -446,9 +540,10 @@ function vlLoadMatchUI(recId) {
                     for(var j = 0; j < matches.length; j++) {
                         var match = matches[j];
                         if(match.eg_record() == item.id && !matchSeenMap[match.id()]) {
-                            item.dest_matchpoint = match.field_type();
-                            var attr = getRecAttrFromMatch(queuedRecordsMap[recId], match);
-                            item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).code();
+                            if(match.match_score)
+                                item.match_score = match.match_score();
+                            item.match_quality = match.quality();
+                            item.rec_quality = queuedRec.quality();
                             matchSeenMap[match.id()] = 1;
                             break;
                         }
@@ -525,6 +620,7 @@ function getRecMatchesFromAttrCode(rec, attrCode) {
 }
 */
 
+/*
 function getRecAttrFromMatch(rec, match) {
     for(var i = 0; i < rec.attributes().length; i++) {
         var attr = rec.attributes()[i];
@@ -532,6 +628,7 @@ function getRecAttrFromMatch(rec, match) {
             return attr;
     }
 }
+*/
 
 function getRecAttrDefFromAttr(attr, type) {
     var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
@@ -568,6 +665,125 @@ function vlFormatViewMatches(id) {
     return '<a href="javascript:void(0);" onclick="vlLoadMatchUI(' + id + ');">' + this.name + '</a>';
 }
 
+function vlGetViewErrors(rowIdx, item) {
+    if(item) {
+        var id = this.grid.store.getValue(item, 'id');
+        var rec = queuedRecordsMap[id];
+        // id:rec_error:item_import_error_count
+        return id + ':' + 
+            (rec.import_error() ? 1 : '') + ':' + 
+            rec.import_items().filter(function(i) {return i.import_error()}).length;
+    }
+    return -1
+}
+
+function vlFormatViewErrors(chunk) {
+    if(chunk == -1) return '';
+    var id = chunk.split(':')[0];
+    var rec = chunk.split(':')[1];
+    var count = chunk.split(':')[2];
+    var links = '';
+    if(rec) 
+        links += '<a href="javascript:void(0);" onclick="vlLoadErrorUI(' + id + ');">Record</a><br/>'; // TODO I18N
+    if(Number(count))
+        links += '<a href="javascript:void(0);" onclick="vlLoadErrorUI(' + id + ');">Items ('+count+')</a>'; // TODO I18N
+    return links;
+}
+
+//var vlItemErrorColumnPicker;
+function vlLoadErrorUI(id) {
+
+    displayGlobalDiv('vl-import-error-div');
+    openils.Util.hide('vl-import-error-grid-all');
+    openils.Util.show('vl-import-error-record');
+
+    var rec = queuedRecordsMap[id];
+
+    dojo.byId('vl-error-id').innerHTML = rec.id();
+    dojo.forEach( // TODO sane authority rec. fields
+        ['title', 'author', 'isbn', 'issn', 'upc'],
+        function(field) {
+            var attr =  getRecAttrFromCode(rec, field);
+            var eid = 'vl-error-' + field;
+            if(attr) {
+                openils.Util.show(dojo.byId(eid).parentNode, 'table-row');
+                dojo.byId(eid).innerHTML = attr.attr_value();
+            } else {
+                openils.Util.hide(dojo.byId(eid).parentNode);
+            }
+        }
+    );
+    var iediv = dojo.byId('vl-error-import-error');
+    var eddiv = dojo.byId('vl-error-error-detail');
+    if(rec.import_error()) {
+        openils.Util.show(iediv.parentNode, 'table-row');
+        openils.Util.show(eddiv.parentNode, 'table-row');
+        iediv.innerHTML = rec.import_error();
+        eddiv.innerHTML = rec.error_detail();
+    } else {
+        openils.Util.hide(iediv.parentNode);
+        openils.Util.hide(eddiv.parentNode);
+    }
+
+    var errorItems = rec.import_items().filter(function(i) {return i.import_error()});
+    if(errorItems.length) {
+        openils.Util.show('vl-import-error-grid-some');
+        storeData = vqbr.toStoreData(errorItems);
+        var store = new dojo.data.ItemFileReadStore({data:storeData});
+        vlImportErrorGrid.setStore(store);
+        vlImportErrorGrid.update();
+    } else {
+        openils.Util.hide('vl-import-error-grid-some');
+    }
+}
+
+function vlLoadErrorUIAll() {
+
+    displayGlobalDiv('vl-import-error-div');
+    openils.Util.hide('vl-import-error-grid-some');
+    openils.Util.hide('vl-import-error-record');
+    openils.Util.show('vl-import-error-grid-all');
+    vlAllImportErrorGrid.resetStore();
+
+    vlImportErrorGrid.displayOffset = 0;
+
+    vlAllImportErrorGrid.dataLoader = function() {
+
+        vlAllImportErrorGrid.showLoadProgressIndicator();
+
+        fieldmapper.standardRequest(
+            ['open-ils.vandelay', 'open-ils.vandelay.import_item.queue.retrieve'],
+            {
+                async : true,
+                params : [
+                    authtoken, currentQueueId, {   
+                        with_import_error: (vlImportItemsShowErrors.checked) ? 1 : null,
+                        offset : vlAllImportErrorGrid.displayOffset,
+                        limit : vlAllImportErrorGrid.displayLimit
+                    }
+                ],
+                onresponse : function(r) {
+                    var item = openils.Util.readResponse(r);
+                    if(!item) return;
+                    vlAllImportErrorGrid.store.newItem(vii.toStoreItem(item));
+                },
+                oncomplete : function() {
+                    vlAllImportErrorGrid.hideLoadProgressIndicator();
+                }
+            }
+        );
+    };
+
+    vlAllImportErrorGrid.dataLoader();
+}
+
+function vlGetOrg(rowIdx, item) {
+    if(!item) return '';
+    var value = this.grid.store.getValue(item, this.field);
+    if(value) return fieldmapper.aou.findOrgUnit(value).shortname();
+    return '';
+}
+
 function vlFormatViewMatchMARC(id) {
     return '<a href="javascript:void(0);" onclick="vlLoadMARCHtml(' + id + ', true, '+
         'function(){displayGlobalDiv(\'vl-match-div\');});">' + this.name + '</a>';
@@ -785,6 +1001,9 @@ var handleRetrieveRecords = function() {
             dojo.byId('vl-queue-summary-name').innerHTML = summary.queue.name();
             dojo.byId('vl-queue-summary-total-count').innerHTML = summary.total +'';
             dojo.byId('vl-queue-summary-import-count').innerHTML = summary.imported + '';
+            dojo.byId('vl-queue-summary-import-item-count').innerHTML = summary.total_items + '';
+            dojo.byId('vl-queue-summary-rec-error-count').innerHTML = summary.rec_import_errors + '';
+            dojo.byId('vl-queue-summary-item-error-count').innerHTML = summary.item_import_errors + '';
         }
     );
 }
@@ -821,10 +1040,12 @@ function vlHandleQueueItemsAction(action) {
             queueItemsImportDialog.hide();
 
             // hack to set the widgets the import funcs will be looking at.  Reset them below.
-            vlUploadQueueAutoImport.attr('value',  vlUploadQueueAutoImport2.attr('value'));
+            vlUploadQueueImportNoMatch.attr('value',  vlUploadQueueImportNoMatch2.attr('value'));
             vlUploadQueueAutoOverlayExact.attr('value',  vlUploadQueueAutoOverlayExact2.attr('value'));
             vlUploadQueueAutoOverlay1Match.attr('value',  vlUploadQueueAutoOverlay1Match2.attr('value'));
             vlUploadMergeProfile.attr('value',  vlUploadMergeProfile2.attr('value'));
+            vlUploadQueueAutoOverlayBestMatch.attr('value',  vlUploadQueueAutoOverlayBestMatch2.attr('value'));
+            vlUploadQueueAutoOverlayBestMatchRatio.attr('value',  vlUploadQueueAutoOverlayBestMatchRatio2.attr('value'));
 
             if(action == 'import') {
                 vlImportSelectedRecords();
@@ -833,14 +1054,18 @@ function vlHandleQueueItemsAction(action) {
             }
             
             // reset the widgets to prevent accidental future actions
-            vlUploadQueueAutoImport.attr('value',  false);
-            vlUploadQueueAutoImport2.attr('value', false);
+            vlUploadQueueImportNoMatch.attr('value',  false);
+            vlUploadQueueImportNoMatch2.attr('value', false);
             vlUploadQueueAutoOverlayExact.attr('value', false);
             vlUploadQueueAutoOverlayExact2.attr('value', false);
             vlUploadQueueAutoOverlay1Match.attr('value', false);
             vlUploadQueueAutoOverlay1Match2.attr('value', false);
             vlUploadMergeProfile.attr('value', '');
             vlUploadMergeProfile2.attr('value', '');
+            vlUploadQueueAutoOverlayBestMatch.attr('value', false);
+            vlUploadQueueAutoOverlayBestMatch2.attr('value', false);
+            vlUploadQueueAutoOverlayBestMatchRatio.attr('value', '0.0');
+            vlUploadQueueAutoOverlayBestMatchRatio2.attr('value', '0.0');
         }
     );
 
@@ -848,8 +1073,8 @@ function vlHandleQueueItemsAction(action) {
 }
     
 
+/* import user-selected records */
 function vlImportSelectedRecords() {
-    displayGlobalDiv('vl-generic-progress-with-total');
     var records = [];
 
     for(var id in selectableGridRecords) {
@@ -861,74 +1086,88 @@ function vlImportSelectedRecords() {
         }
     }
 
+    vlImportRecordQueue(
+        currentType, 
+        currentQueueId, 
+        records,
+        function(){
+            retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
+        }
+    );
+}
+
+/* import all (non-imported) queue records */
+function vlImportAllRecords() {
+    vlImportRecordQueue(
+        currentType, 
+        currentQueueId, 
+        null,
+        function(){
+            retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
+        }
+    );
+}
+
+/* if recList has values, import only those records */
+function vlImportRecordQueue(type, queueId, recList, onload) {
+    displayGlobalDiv('vl-generic-progress-with-total');
+
+    /* set up options */
+    var mergeOpt = false;
     var options = {overlay_map : currentOverlayRecordsMap};
 
+    if(vlUploadQueueImportNoMatch.checked) {
+        options.import_no_match = true;
+        vlUploadQueueImportNoMatch.checked = false;
+    }
+
     if(vlUploadQueueAutoOverlayExact.checked) {
         options.auto_overlay_exact = true;
         vlUploadQueueAutoOverlayExact.checked = false;
+        mergeOpt = true;
+    }
+
+    if(vlUploadQueueAutoOverlayBestMatch.checked) {
+        options.auto_overlay_best_match = true;
+        vlUploadQueueAutoOverlayBestMatch.checked = false;
+        options.match_quality_ratio = vlUploadQueueAutoOverlayBestMatchRatio.attr('value');
+        mergeOpt = true;
     }
 
     if(vlUploadQueueAutoOverlay1Match.checked) {
         options.auto_overlay_1match = true;
         vlUploadQueueAutoOverlay1Match.checked = false;
+        mergeOpt = true;
     }
-    
+
     var profile = vlUploadMergeProfile.attr('value');
     if(profile != null && profile != '') {
         options.merge_profile = profile;
     }
 
-    fieldmapper.standardRequest(
-        ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
-        {   async: true,
-            params: [authtoken, records, options],
-            onresponse: function(r) {
-                var resp = r.recv().content();
-                if(e = openils.Event.parse(resp))
-                    return alert(e);
-                if(resp.complete) {
-                    return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
-                } else {
-                    vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
-                }
-            }, 
-        }
-    );
-}
-
-function vlImportAllRecords() {
-    vlImportRecordQueue(currentType, currentQueueId, false,
-        function(){displayGlobalDiv('vl-queue-div');});
-}
+    /* determine which method we're calling */
 
-function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
-    displayGlobalDiv('vl-generic-progress-with-total');
     var method = 'open-ils.vandelay.bib_queue.import';
-    if(noMatchOnly)
-        method = method.replace('import', 'nomatch.import');
     if(type == 'auth')
         method = method.replace('bib', 'auth');
 
-    var options = {};
-    if(vlUploadQueueAutoOverlayExact.checked) {
-        options.auto_overlay_exact = true;
-        vlUploadQueueAutoOverlayExact.checked = false;
+    if(!mergeOpt) {
+        // in the interest of speed, if no merge options are 
+        // chosen, tell the back-end code to only process records
+        // that have no matches
+        method = method.replace(/\.import/, '.nomatch.import');
     }
 
-    if(vlUploadQueueAutoOverlay1Match.checked) {
-        options.auto_overlay_1match = true;
-        vlUploadQueueAutoOverlay1Match.checked = false;
-    }
-    
-    var profile = vlUploadMergeProfile.attr('value');
-    if(profile != null && profile != '') {
-        options.merge_profile = profile;
+    var params = [authtoken, queueId, options];
+    if(recList) {
+        method = 'open-ils.vandelay.'+currentType+'_record.list.import';
+        params[1] = recList;
     }
 
     fieldmapper.standardRequest(
         ['open-ils.vandelay', method],
         {   async: true,
-            params: [authtoken, queueId, options],
+            params: params,
             onresponse: function(r) {
                 var resp = r.recv().content();
                 if(e = openils.Event.parse(resp))
@@ -949,16 +1188,20 @@ function batchUpload() {
     currentType = dijit.byId('vl-record-type').getValue();
 
     var handleProcessSpool = function() {
-        if(vlUploadQueueAutoImport.checked || vlUploadQueueAutoOverlayExact.checked || vlUploadQueueAutoOverlay1Match.checked) {
-            var noMatchOnly = !vlUploadQueueAutoOverlayExact.checked && !vlUploadQueueAutoOverlay1Match.checked;
-            vlImportRecordQueue(
-                currentType, 
-                currentQueueId, 
-                noMatchOnly,
-                function() {
-                    retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
-                }
-            );
+        if( 
+            vlUploadQueueImportNoMatch.checked || 
+            vlUploadQueueAutoOverlayExact.checked || 
+            vlUploadQueueAutoOverlay1Match.checked ||
+            vlUploadQueueAutoOverlayBestMatch.checked ) {
+
+                vlImportRecordQueue(
+                    currentType, 
+                    currentQueueId, 
+                    null,
+                    function() {
+                        retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
+                    }
+                );
         } else {
             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
         }
@@ -978,7 +1221,10 @@ function batchUpload() {
         currentQueueId = vlUploadQueueSelector.getValue();
         uploadMARC(handleUploadMARC);
     } else {
-        createQueue(queueName, currentType, handleCreateQueue, vlUploadQueueHoldingsImportProfile.attr('value'));
+        createQueue(queueName, currentType, handleCreateQueue, 
+            vlUploadQueueHoldingsImportProfile.attr('value'),
+            vlUploadQueueMatchSet.attr('value')
+        );
     }
 }
 
@@ -990,6 +1236,50 @@ function vlFleshQueueSelect(selector, type) {
     selector.setDisplayedValue('');
     if(data[0])
         selector.setValue(data[0].id());
+
+    var qInput = dijit.byId('vl-queue-name');
+
+    var selChange = function(val) {
+        console.log('selector onchange');
+        // user selected a queue from the selector;  clear the input and 
+        // set the item import profile already defined for the queue
+        var queue = allUserBibQueues.filter(function(q) { return (q.id() == val) })[0];
+        if(val) {
+            vlUploadQueueHoldingsImportProfile.attr('value', queue.item_attr_def() || '');
+            vlUploadQueueHoldingsImportProfile.attr('disabled', true);
+            vlUploadQueueMatchSet.attr('value', queue.match_set() || '');
+            vlUploadQueueMatchSet.attr('disabled', true);
+        } else {
+            vlUploadQueueHoldingsImportProfile.attr('value', '');
+            vlUploadQueueHoldingsImportProfile.attr('disabled', false);
+            vlUploadQueueMatchSet.attr('value', '');
+            vlUploadQueueMatchSet.attr('disabled', false);
+        }
+        dojo.disconnect(qInput._onchange);
+        qInput.attr('value', '');
+        qInput._onchange = dojo.connect(qInput, 'onChange', inputChange);
+    }
+    
+    var inputChange = function(val) {
+        console.log('qinput onchange');
+        // user entered a new queue name. clear the selector 
+        vlUploadQueueHoldingsImportProfile.attr('value', '');
+        vlUploadQueueHoldingsImportProfile.attr('disabled', false);
+        vlUploadQueueMatchSet.attr('value', '');
+        vlUploadQueueMatchSet.attr('disabled', false);
+        dojo.disconnect(selector._onchange);
+        selector.attr('value', '');
+        selector._onchange = dojo.connect(selector, 'onChange', selChange);
+    }
+
+    selector._onchange = dojo.connect(selector, 'onChange', selChange);
+    qInput._onchange = dojo.connect(qInput, 'onChange', inputChange);
+}
+
+function vlUpdateMatchSetSelector(type) {
+    type = (type.match(/bib/)) ? 'biblio' : 'authority';
+    vlUploadQueueMatchSet.store = 
+        new dojo.data.ItemFileReadStore({data:vms.toStoreData(matchSets[type])});
 }
 
 function vlShowUploadForm() {
@@ -1000,6 +1290,30 @@ function vlShowUploadForm() {
     vlUploadSourceSelector.setValue(vlBibSources[0].id());
     vlUploadQueueHoldingsImportProfile.store = 
         new dojo.data.ItemFileReadStore({data:viiad.toStoreData(importItemDefs)});
+    vlUpdateMatchSetSelector(vlUploadRecordType.getValue());
+
+    // use ratio from the merge profile if it's set
+    dojo.connect(
+        vlUploadMergeProfile, 
+        'onChange',
+        function(val) {
+            if(!val) return;
+            var profile = mergeProfiles.filter(function(p) { return (p.id() == val); })[0];
+            if(profile.lwm_ratio() != null)
+               vlUploadQueueAutoOverlayBestMatchRatio.attr('value', profile.lwm_ratio()+''); 
+        }
+    );
+    dojo.connect(
+        vlUploadMergeProfile2, 
+        'onChange',
+        function(val) {
+            if(!val) return;
+            var profile = mergeProfiles.filter(function(p) { return (p.id() == val); })[0];
+            if(profile.lwm_ratio() != null)
+               vlUploadQueueAutoOverlayBestMatchRatio2.attr('value', profile.lwm_ratio()+''); 
+        }
+    );
+
 }
 
 function vlShowQueueSelect() {
@@ -1007,6 +1321,17 @@ function vlShowQueueSelect() {
     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
 }
 
+function vlShowMatchSetEditor() {
+    displayGlobalDiv('vl-match-set-editor-div');
+    dojo.byId('vl-match-set-editor-div').appendChild(
+        dojo.create('iframe', {
+            id : 'vl-match-set-iframe',
+            src : oilsBasePath + '/eg/conify/global/vandelay/match_set',
+            style : 'width:100%; height:500px; border:none; margin:0px;'
+        })
+    );
+}
+
 function vlFetchQueueFromForm() {
     currentType = vlQueueSelectType.getValue();
     currentQueueId = vlQueueSelectQueueList.getValue();
@@ -1163,7 +1488,6 @@ function onAttrEditorClick() {
     var parsed_xpath = xpathParser.parse(this.store.getValue(row, 'xpath'));
     dijit.byId('attr-editor-tags').attr('value', parsed_xpath.tags);
     dijit.byId('attr-editor-subfields').attr('value', parsed_xpath.subfields);
-    dijit.byId('attr-editor-identifier').attr('value', this.store.getValue(row, 'ident'));
     dijit.byId('attr-editor-xpath').attr('value', this.store.getValue(row, 'xpath'));
     dijit.byId('attr-editor-remove').attr('value', this.store.getValue(row, 'remove'));
 
index 920db0d..3409f3c 100644 (file)
 <!ENTITY staff.main.menu.admin.server_admin.conify.z3950_source.label "Z39.50 Servers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.circulation_modifier.label "Circulation Modifiers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_setting_type "Organization Unit Setting Types">
+<!ENTITY staff.main.menu.admin.server_admin.conify.import_match_set "Import Match Sets">
 <!ENTITY staff.main.menu.admin.server_admin.conify.usr_setting_type "User Setting Types">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_hard_due_date "Hard Due Date Changes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_circ_duration "Circulation Duration Rules">
index 31fdc67..d2d1789 100644 (file)
@@ -1,9 +1,12 @@
 <!ENTITY vandelay.add.existing.queue "or Add to an Existing Queue">
 <!ENTITY vandelay.auth.attrs "Authority attributes">
 <!ENTITY vandelay.auth.records "Authority Records">
-<!ENTITY vandelay.auto.import.noncolliding "Auto-Import Non-Colliding Records">
-<!ENTITY vandelay.auto.import.auto_overlay_exact "Auto Merge/Overlay Exact Matches">
-<!ENTITY vandelay.auto.import.auto_overlay_1match "Auto Merge/Overlay When Exactly 1 Match is Found">
+<!ENTITY vandelay.auto.import.noncolliding "Import Non-Colliding Records">
+<!ENTITY vandelay.auto.import.auto_overlay_exact "Merge/Overlay Exact Matches (901c)">
+<!ENTITY vandelay.auto.import.auto_overlay_1match "Merge/Overlay Single Matches">
+<!ENTITY vandelay.auto.import.auto_overlay_best "Merge/Overlay Best Match">
+<!ENTITY vandelay.auto.import.auto_overlay_best_ratio "Best Match Minimum Quality Ratio">
+<!ENTITY vandelay.auto.import.auto_overlay_best_ratio.desc "New Record Quaility / Quality of Best Match">
 <!ENTITY vandelay.auto.import.merge_profile "Merge/Overlay Profile">
 <!ENTITY vandelay.auto.width "Auto Width">
 <!ENTITY vandelay.back.to.import.queue "Back To Import Queue">
 <!ENTITY vandelay.dest.match.point "Destination Match Point">
 <!ENTITY vandelay.display "Display">
 <!ENTITY vandelay.done "Done">
-<!ENTITY vandelay.edit.attributes "Edit Attributes">
-<!ENTITY vandelay.edit.attrs "Edit Attributes">
-<!ENTITY vandelay.edit.profiles "Edit Merge / Overlay Profiles">
-<!ENTITY vandelay.edit.import_item_attrs "Edit Import Item Attributes">
+<!ENTITY vandelay.edit.attributes "Record Display Attributes">
+<!ENTITY vandelay.edit.attrs "Record Display Attributes">
+<!ENTITY vandelay.edit.profiles "Merge / Overlay Profiles">
+<!ENTITY vandelay.edit.match_set "Record Match Sets">
+<!ENTITY vandelay.edit.import_item_attrs "Import Item Attributes">
 <!ENTITY vandelay.false "False">
 <!ENTITY vandelay.file.to.upload "File to Upload:">
 <!ENTITY vandelay.for.example "Example">
 <!ENTITY vandelay.id "ID">
 <!ENTITY vandelay.import.matches "Import Matches">
 <!ENTITY vandelay.import.records "Import Records">
-<!ENTITY vandelay.import.selected "Import Selected">
-<!ENTITY vandelay.import.all "Import All">
+<!ENTITY vandelay.import.selected "Import Selected Records">
+<!ENTITY vandelay.import.all "Import All  Records">
 <!ENTITY vandelay.import.time "Import Time">
 <!ENTITY vandelay.inspect.queue "Inspect Queue">
 <!ENTITY vandelay.last.edit.date "Last Edit Date">
-<!ENTITY vandelay.limit.to.collision.matches "Limit to Collision Matches">
+<!ENTITY vandelay.limit.to.collision.matches "Limit to Records with Matches">
 <!ENTITY vandelay.limit.to.non.imported "Limit to Non-Imported Records">
+<!ENTITY vandelay.limit.to.import_error "Limit to Records with Import Errors">
 <!ENTITY vandelay.marc.file.upload "Evergreen MARC File Upload">
 <!ENTITY vandelay.marc.record "MARC Record">
 <!ENTITY vandelay.matches "Matches">
-<!ENTITY vandelay.next.page "Next Page &#187;">
+<!ENTITY vandelay.next.page "Next &#187;">
 <!ENTITY vandelay.overlay.selected.record "Overlay selected record with imported record">
 <!ENTITY vandelay.overlay.target "Overlay Target">
 <!ENTITY vandelay.page "Page">
 <!ENTITY vandelay.powered.by.evergreen "Powered by Evergreen!">
-<!ENTITY vandelay.prev.page "&#171; Previous Page">
+<!ENTITY vandelay.prev.page "&#171; Previous">
 <!ENTITY vandelay.processing "Processing... ">
 <!ENTITY vandelay.queue "Queue">
 <!ENTITY vandelay.queue.type "Queue Type">
@@ -60,7 +65,7 @@
 <!ENTITY vandelay.refresh "Refresh">
 <!ENTITY vandelay.remove.advanced "Remove (advanced)">
 <!ENTITY vandelay.remove "Remove">
-<!ENTITY vandelay.results.per.page "Results Per Page">
+<!ENTITY vandelay.results.per.page "Records Per Page">
 <!ENTITY vandelay.retrieve.queue "Retrieve Queue">
 <!ENTITY vandelay.return "Return">
 <!ENTITY vandelay.select.cols "Select Columns">
 <!ENTITY vandelay.export.field_no_hint "(starting from 0)">
 <!ENTITY vandelay.export.bucket "Record Bucket ID">
 <!ENTITY vandelay.select_actions "-- Actions --">
-<!ENTITY vandelay.queue.total "Total:">
-<!ENTITY vandelay.queue.imported "Imported:">
+<!ENTITY vandelay.queue.total "Records in Queue:">
+<!ENTITY vandelay.queue.imported "Records Imported:">
 <!ENTITY vandelay.queue.column_picker.title "Column Picker">
 <!ENTITY vandelay.import.bib_sources "Select a Record Source">
diff --git a/Open-ILS/web/templates/default/conify/global/vandelay/match_set.tt2 b/Open-ILS/web/templates/default/conify/global/vandelay/match_set.tt2
new file mode 100644 (file)
index 0000000..fad64db
--- /dev/null
@@ -0,0 +1,94 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Vandelay Match Sets' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class="oils-header-panel">
+        <div>[% ctx.page_title %]</div>
+        <div>
+            <button dojoType="dijit.form.Button"
+                onClick="vms_grid.showCreateDialog()">New Match Set</button>
+            <button dojoType="dijit.form.Button"
+                onClick="vms_grid.deleteSelected()">Delete Selected</button>
+        </div>
+    </div>
+    <div>
+        Show sets owned at or below:
+        <select dojoType="openils.widget.OrgUnitFilteringSelect"
+            jsId="context_org_selector"></select>
+    </div>
+    <table jsId="vms_grid"
+        dojoType="openils.widget.AutoGrid"
+        query="{id: '*'}"
+        defaultCellWidth="'16em'"
+        fmClass="vms"
+        fieldorder="['name', 'owner']"
+        suppressEditFields="['id']"
+        showPaginator="true"
+        editOnEnter="true">
+        <thead>
+            <tr>
+                <th field="name" get="field_plus_id" formatter="tree_editor_link"></th>
+                <th field="owner" get="openils.widget.AutoGrid.orgUnitGetter">
+                </th>
+            </tr>
+        </thead>
+    </table>
+</div>
+<div class="hidden">
+    <select dojoType="dijit.form.FilteringSelect" jsId="mtype_selector">
+        [%# for the origin of these hard coded options, see the definition
+        of vandelay.match_set.mtype in 012.schema.vandelay.sql %]
+        <option value="biblio">biblio</option>
+        <option value="authority">authority</option>
+        <!-- XXX: nah <option value="mfhd">mfhd</option> -->
+    </select>
+</div>
+
+<script type="text/javascript">
+    dojo.require("dijit.form.FilteringSelect");
+    dojo.require("openils.widget.AutoGrid");
+    dojo.require("openils.widget.OrgUnitFilteringSelect");
+
+    var context_org;
+
+    function load_grid(search) {
+        if (!search) search = {"id": {"!=": null}};
+
+        vms_grid.loadAll({"order_by": {"vms": "name"}}, search);
+    }
+
+    function field_plus_id(rowIndex, item) {
+        if (!item) return null;
+        var datum = {};
+        datum[this.field] = this.grid.store.getValue(item, this.field);
+        datum.id = this.grid.store.getValue(item, "id");
+        return datum;
+    }
+
+    function tree_editor_link(datum) {
+        if (!datum) return "";
+        return '<a href="[% ctx.base_path %]/eg/conify/global/vandelay/match_set_tree?match_set=' +
+            datum.id + '">' + datum.name + '</a>';
+    }
+
+    openils.Util.addOnLoad(
+        function() {
+            new openils.User().buildPermOrgSelector(
+                "ADMIN_IMPORT_MATCH_SET", context_org_selector,
+                null, function() {
+                    context_org_selector.onChange = function() {
+                        context_org = this.attr("value");
+                        vms_grid.resetStore();
+                        load_grid({
+                            "owner": aou.descendantNodeList(context_org, true)
+                        });
+                    };
+                }
+            );
+
+            vms_grid.overrideEditWidgets.mtype = mtype_selector;
+            vms_grid.overrideEditWidgets.mtype.shove = {"create": "biblio"};
+            load_grid();
+        }
+    );
+</script>
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/vandelay/match_set_tree.tt2 b/Open-ILS/web/templates/default/conify/global/vandelay/match_set_tree.tt2
new file mode 100644 (file)
index 0000000..8ad1eea
--- /dev/null
@@ -0,0 +1,150 @@
+[% WRAPPER 'default/base.tt2' %]
+[% ctx.page_title = 'Vandelay Match Set Editor' %]
+<style type="text/css">
+    h1 { margin: 1ex 0; }
+    .outer { clear: both; margin-bottom: 1ex; }
+    .space-me input { margin: 0 1em; }
+    button { margin: 0 0.5em; }
+    input[type=submit] { padding: 0 0.5em; }
+    #tree-here { margin-bottom: 1.5em; }
+    #vms-table { padding-bottom: 2ex; }
+    #vms-table th { text-align: right; }
+    #vms-table td { padding-left: 1em; }
+    #src-pane { float: left; width: 49%; }
+    #tree-pane { float: right; width: 50%; }
+    #submit-row { clear: both; text-align: center; padding-top: 1.5ex; }
+    #submit-row hr { margin: 1.5ex 0; }
+    #expr-preview-row { margin: 2ex 0; }
+    #expr-preview {
+        font-family: monospace;
+        font-size: 125%;
+        font-weight: bold;
+        background-color: #000066;
+        color: white;
+    }
+    .node-editor { margin-bottom: 1.5ex; }
+    .node-editor td { padding: 0.5ex; }
+    li { background-color: #ddd; }
+    .replace-mode { background-color: #990; color: #fff; }
+</style>
+<h1>[% ctx.page_title %]</h1>
+<table id="vms-table">
+    <tbody>
+        <tr>
+            <th>Match set name:</th>
+            <td><strong id="vms-name"></strong></td>
+        </tr>
+        <tr>
+            <th>Owning Library:</th>
+            <td id="vms-owner"></td>
+        </tr>
+        <tr>
+            <th>Type:</th>
+            <td id="vms-mtype"></td>
+        </tr>
+    </tbody>
+</table>
+<hr />
+<!-- XXX TODO
+    give some indication of which match set we're editing the tree for
+    -->
+<table class="hidden">
+    <tr quality-controls="1">
+        <td>
+            <label for="value-input">Value:</label>
+        </td>
+        <td>
+            <input id="value-input" type="text" fmfield="value" />
+        </td>
+    </tr>
+    <tr consistent-controls="1">
+        <td>
+            <label for="quality-input"
+                title="A relative number representing the impact of this expression on the quality of the overall record match"><!-- XXX tooltipize -->
+                Quality:
+            </label>
+        </td>
+        <td>
+            <input id="quality-input" type="text" value="1"
+                size="4" maxlength="3" fmfield="quality" />
+        </td>
+    </tr>
+    <tr point-controls="1">
+        <td>
+            <label for="negate-input">Negate?</label>
+        </td>
+        <td>
+            <input id="negate-input" type="checkbox" fmfield="negate" />
+        </td>
+    </tr>
+</table>
+<div class="outer">
+    <div id="expr-preview-row">
+        <em>Your Expression:</em>
+        <span id="expr-preview"></span>
+    </div>
+    <div id="vmsp-buttons">
+        Add new
+        <button onclick="node_editor.add('svf');">Record Attribute</button>
+        <button onclick="node_editor.add('tag');">MARC Tag and Subfield</button>
+        <button onclick="node_editor.add('bool_op');">Boolean Operator</button>
+    </div>
+</div>
+<div class="outer" style="margin-top: 2ex;">
+    <div id="src-pane">
+        <div><big>Working Match Point</big></div>
+        <div>
+            <form id="node-editor-container" onsubmit="return false;"></form>
+        </div>
+        <ul id="src-here"></ul>
+    </div>
+
+    <div id="tree-pane">
+        <div><big>Your Expression</big></div>
+        <div id="tree-here"></div>
+        <div>
+            <button id="deleter" onclick="delete_selected_in_tree()">
+                Delete Selected Node
+            </button>
+            <button id="replacer" onclick="replace_mode()"></button>
+        </div>
+    </div>
+</div>
+<div id="submit-row">
+    <button onclick="save_tree()">Save Changes To Expression</button>
+</div>
+<hr />
+<div id="quality-editor-wrapper">
+    <div>
+        <div style="float: left; width: 50%;">
+            <big>Quality Metrics for this Match Set</big>
+        </div>
+        <div style="float: right; width: 49%; text-align: right;">
+            <button onclick="qnode_editor.add('svf')">
+                Record Attribute
+            </button>
+            <button onclick="qnode_editor.add('tag')">
+                MARC Tag and Subfield
+            </button>
+            <button onclick="vmsq_grid.deleteSelected()">
+                Delete Selected Metrics
+            </button>
+        </div>
+    </div>
+    <br style="clear: both;" />
+    <table id="qnode-editor-container"></table>
+    <table jsId="vmsq_grid"
+        dojoType="openils.widget.AutoGrid"
+        query="{id: '*'}"
+        defaultCellWidth="'17%'"
+        fmClass="vmsq"
+        fieldOrder="['quality','svf','tag','subfield','value']"
+        suppressFields="['match_set']"
+        showPaginator="true"
+        editOnEnter="false">
+    </table>
+</div>
+<div jsId="progress_dialog" dojoType="openils.widget.ProgressDialog"></div>
+<script type="text/javascript"
+    src="[% ctx.media_prefix %]/js/ui/default/conify/global/vandelay/match_set.js"></script>
+[% END %]
index d06b176..1a09b0b 100644 (file)
                 </td>
             </tr>
             <tr>
-                <td><label for="ident">&vandelay.id.field;: </label></td>
-                <td>
-                    <select dojoType="dijit.form.FilteringSelect" name="ident" id="attr-editor-identifier">
-                        <option value='f' selected='selected'>&vandelay.false;</option>
-                        <option value='t'>&vandelay.true;</option>
-                    </select>
-                </td>
-            </tr>
-            <tr>
                 <td><label for="attr-editor-xpath">&vandelay.xpath.advanced;: </label></td>
 
                 <td><input dojoType="dijit.form.TextBox" id="attr-editor-xpath" name="xpath"></input></td>
@@ -90,7 +81,6 @@
                 <th field='description' width='auto'>&vandelay.descrip;</th>
                 <th field='tag' get='attrGridGetTag'>&vandelay.tag;</th>
                 <th field='subfield' get='attrGridGetSubfield'>&vandelay.subfield;</th>
-                <th field='ident'>&vandelay.identifier;</th>
                 <th field='xpath' width='auto'>&vandelay.xpath;</th>
                 <th field='remove' width='auto'>&vandelay.remove;</th>
             </tr>
diff --git a/Open-ILS/web/templates/default/vandelay/inc/import_errors.tt2 b/Open-ILS/web/templates/default/vandelay/inc/import_errors.tt2
new file mode 100644 (file)
index 0000000..5554f00
--- /dev/null
@@ -0,0 +1,77 @@
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
+    <button dojoType='dijit.form.Button' 
+        onclick="displayGlobalDiv('vl-queue-div');">&#x2196; &vandelay.back.to.import.queue;</button>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
+    <div id='vl-import-error-record' class='hidden'>
+        <h1>Import Errors</h1><br/>
+        <table>
+            <tbody>
+                <tr><td>ID</td><td id='vl-error-id'/></tr>
+                <tr><td>Import Error</td><td id='vl-error-import-error'/></tr>
+                <tr><td>Error Detail</td><td id='vl-error-error-detail'/></tr>
+                <tr><td>Title</td><td id='vl-error-title'/></tr>
+                <tr><td>Author</td><td id='vl-error-author'/></tr>
+                <tr><td>ISBN</td><td id='vl-error-isbn'/></tr>
+                <tr><td>ISSN</td><td id='vl-error-issn'/></tr>
+                <tr><td>UPC</td><td id='vl-error-upc'/></tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
+    <div class='hidden' id='vl-import-error-grid-some'>
+        <table  jsId="vlImportErrorGrid"
+                dojoType="openils.widget.AutoGrid"
+                autoHeight='true'
+                fieldOrder="['barcode', 'call_number', 'owning_lib', 'import_error', 'error_detail']"
+                query="{id: '*'}"
+                hidePaginator='true'
+                showColumnPicker='true'
+                columnPickerPrefix='"vandelay.item.import_error"'
+                fmClass='vii'>
+                <thead>
+                    <tr>
+                        <th field='owning_lib' get='vlGetOrg'/>
+                        <th field='circ_lib' get='vlGetOrg'/>
+                    </tr>
+                </thead>
+        </table>
+    </div>
+</div>
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
+    <div class='hidden' id='vl-import-error-grid-all'>
+        <h1>Import Items</h1><br/>
+        <input dojoType='dijit.form.CheckBox' jsId='vlImportItemsShowErrors' onchange='vlLoadErrorUIAll();'/>
+        <span>Limit to Import Failures</span>
+        <table width='100%'><tr><td width='100%' align='right'>
+            <select id='vl-item-export-options' style='margin-right: 10px;'>
+                <!-- TODO I18N -->
+                <option value=''>Export Items As...</option>
+                <option value='print'>Print</option>
+                <option value='csv'>CSV</option>
+                <option value='email'>Email</option>
+            </select>
+        </td></tr></table>
+        <table  jsId="vlAllImportErrorGrid"
+                dojoType="openils.widget.AutoGrid"
+                autoHeight='true'
+                fieldOrder="['barcode', 'call_number', 'owning_lib', 'import_error', 'error_detail']"
+                query="{id: '*'}"
+                showPaginator='true'
+                showColumnPicker='true'
+                columnPickerPrefix='"vandelay.item.import_error"'
+                fmClass='vii'>
+                <thead>
+                    <tr>
+                        <th field='owning_lib' get='vlGetOrg'/>
+                        <th field='circ_lib' get='vlGetOrg'/>
+                    </tr>
+                </thead>
+        </table>
+    </div>
+</div>
+
index 90f3546..2376dbc 100644 (file)
@@ -8,18 +8,15 @@
                     name: '&vandelay.overlay.target;', 
                     get: vlGetOverlayTargetSelector,
                     formatter : vlFormatOverlayTargetSelector,
-                    /*
-                    value: '&lt;input type="checkbox" name="vl-overlay-target-RECID" '+
-                        'onclick="vlHandleOverlayTargetSelected(ID, GRIDID);" gridid="GRIDID" match="ID"/>'
-                        */
                 },
-                {name:'&vandelay.source.match.point;', field:'src_matchpoint'},
-                {name:'&vandelay.dest.match.point;', field:'dest_matchpoint'},
                 {name: '&vandelay.id;', field:'id'},
                 {   name: '&vandelay.view.marc;', 
                     get: vlGetViewMARC, 
                     formatter : vlFormatViewMatchMARC
                 },
+                {name: 'Match Score', field:'match_score'},
+                {name: 'Queued Record Quality', field:'rec_quality'},
+                {name: 'Matched Record Quality', field:'match_quality'},
                 {name: '&vandelay.creator;', get: vlGetCreator},
                 {name: '&vandelay.create.date;', field:'create_date', get: vlGetDateTimeField},
                 {name: '&vandelay.last.edit.date;', field:'edit_date', get: vlGetDateTimeField},
index 5bd4d4d..ca67f3a 100644 (file)
@@ -13,9 +13,9 @@
     </div>
     <table  jsId="pGrid"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['name', 'owner', 'preserve_spec', 'replace_spec', 'add_spec', 'strip_spec']"
+            fieldOrder="['name', 'owner', 'preserve_spec', 'replace_spec', 'add_spec', 'strip_spec', 'lwm_ratio']"
             query="{id: '*'}"
-            defaultCellWidth='"auto"'
+            defaultCellWidth='"14%"'
             fmClass='vmp'
             showPaginator='true'
             editOnEnter='true'>
index e7d6eaf..c34c57f 100644 (file)
-<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
-    <table class='wide'>
-        <tr>
-            <td align='left'>
-                <h1>&vandelay.record.queue; <span style='font-style:italic;' id='vl-queue-summary-name'/></h1><br/>
-            </td>
-            <td align='right'>
-                &vandelay.queue.total; <span style='font-weight:bold;' id='vl-queue-summary-total-count'/>
-                &vandelay.queue.imported; <span style='font-weight:bold;' id='vl-queue-summary-import-count'/>
-            </td>
-        </tr>
-    </table>
-</div>
-
-<br/>
-
-<div jsId='queueItemsImportDialog' dojoType="dijit.Dialog" title="Import Items">
-    <div dojoType="dijit.layout.ContentPane">
-        <table class='form_table'>
-            <tbody>
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client' style='margin-top:10px;'>
+    <fieldset id='vl-queue-filter-fieldset'>
+        <legend>Queue <span style='font-style:italic;' id='vl-queue-summary-name'/></legend>
+        <table width='100%'><tr>
+            <td> <!-- big left td -->
+            <table>
                 <tr>
-                    <td>&vandelay.auto.import.noncolliding;</td>
-                    <td colspan='4'>
-                        <input jsId='vlUploadQueueAutoImport2' dojoType='dijit.form.CheckBox'/>
-                    </td>
-                </tr>
-                <tr>
-                    <td>&vandelay.auto.import.auto_overlay_exact;</td>
-                    <td colspan='4'>
-                        <input jsId='vlUploadQueueAutoOverlayExact2' dojoType='dijit.form.CheckBox'/>
-                    </td>
-                </tr>
-                <tr>
-                    <td>&vandelay.auto.import.auto_overlay_1match;</td>
-                    <td colspan='4'>
-                        <input jsId='vlUploadQueueAutoOverlay1Match2' dojoType='dijit.form.CheckBox'/>
-                    </td>
-                </tr>
-                <tr>
-                    <td>&vandelay.auto.import.merge_profile;</td>
-                    <td colspan='4'>
-                        <div jsId='vlUploadMergeProfile2' 
-                            dojoType='dijit.form.FilteringSelect' required='false' labelAttr='name' searchAttr='name'/>
+                    <td valign='top'>
+                        <table class='queue-nav-table'>
+                            <thead><tr><th colspan='2' class='queue-nav-table-label'>Queue Actions</th></tr></thead>
+                            <tbody>
+                                <tr><td><a href='javascript:;' onclick='vlHandleQueueItemsAction("import")'>&vandelay.import.selected;</a></td></tr>
+                                <tr><td><a href='javascript:;' onclick='vlHandleQueueItemsAction("import_all")'>&vandelay.import.all;</a></td></tr>
+                                <tr><td><a href='javascript:;' onclick='vlLoadErrorUIAll();'>View Import Items</a></td></tr>
+                                <tr><td><a href='javascript:;' onclick='
+                                    if(confirm("&vandelay.sure.to.delete.queue;")) {
+                                        vlDeleteQueue(currentType, currentQueueId, 
+                                            function() { displayGlobalDiv("vl-marc-upload-div"); });}'>&vandelay.delete.queue;</a></td></tr>
+                            </tbody>
+                        </table>
                     </td>
-                </tr>
-                <tr>
-                    <td>
-                        <button dojoType='dijit.form.Button' jsId='queueItemsImportCancelButton'>Cancel</button>
+
+                    <td valign='top'>
+                        <table  id='vl-queue-summary-table' class='queue-nav-table'>
+                            <thead><tr><th colspan='2' class='queue-nav-table-label'>Queue Summary</th></tr></thead>
+                            <tbody>
+                                <tr><td>&vandelay.queue.total;</td><td> <span style='font-weight:bold;' id='vl-queue-summary-total-count'/></td></tr>
+                                <tr><td>&vandelay.queue.imported;</td><td> <span style='font-weight:bold;' id='vl-queue-summary-import-count'/></td></tr>
+                                <tr><td>Record Import Failures</td><td> <span style='font-weight:bold;' id='vl-queue-summary-rec-error-count'/></td></tr>
+                                <tr><td>Items in Queue</td><td> <span style='font-weight:bold;' id='vl-queue-summary-import-item-count'/></td></tr>
+                                <tr><td>Item Import Failures</td><td> <span style='font-weight:bold;' id='vl-queue-summary-item-error-count'/></td></tr>
+                            </tbody>
+                        </table>
                     </td>
-                    <td>
-                        <button dojoType='dijit.form.Button' jsId='queueItemsImportGoButton'>Import</button>
+
+                    <td valign='top'> <!-- filters -->
+                        <table id='vl-queue-filter-table' class='queue-nav-table'>
+                            <thead><tr><th colspan='2' class='queue-nav-table-label'>Queue Filters</th></tr></thead>
+                            <tbody>
+                                <tr>
+                                    <td>&vandelay.limit.to.collision.matches;</td>
+                                    <td>
+                                        <input dojoType='dijit.form.CheckBox' 
+                                            jsId='vlQueueGridShowMatches' onchange='retrieveQueuedRecords();'/>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>&vandelay.limit.to.non.imported;</td>
+                                    <td>
+                                        <input dojoType='dijit.form.CheckBox' 
+                                            jsId='vlQueueGridShowNonImport' onchange='retrieveQueuedRecords();'/>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>&vandelay.limit.to.import_error;</td>
+                                    <td>
+                                        <input dojoType='dijit.form.CheckBox' 
+                                            jsId='vlQueueGridShowImportErrors' onchange='retrieveQueuedRecords();'/>
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table>
                     </td>
                 </tr>
-            </tbody>
-        </table>
-    </div>
-</div>
+            </table>
+        </td>
 
-<!-- queue grid navigation row -->
-<div dojoType="dijit.layout.ContentPane" layoutAlign='client'>
-
-    <table width='100%' style='margin-bottom:0px;'>
-        <tr>
-            <td align='left' valign='bottom'>
-                <select id='vl-queue-actions-selector'>
-                    <option selected='selected' disabled='disabled' value='select-actions'>&vandelay.select_actions;</option>
-                    <option value='import'>&vandelay.import.selected;</option>
-                    <option value='import_all'>&vandelay.import.all;</option>
-                    <option value='delete_queue'>&vandelay.delete.queue;</option>
-                </select>
-                <script type="text/javascript">
-                    var sel = dojo.byId('vl-queue-actions-selector');
-                    sel.onchange = function(evt) {
-                        switch(openils.Util.selectorValue(evt.target)) {
-                            case 'import': vlHandleQueueItemsAction('import'); break;;
-                            case 'import_all': vlHandleQueueItemsAction('import_all'); break;;
-                            case 'delete_queue': 
-                                if(confirm('&vandelay.sure.to.delete.queue;')) {
-                                    vlDeleteQueue(currentType, currentQueueId, 
-                                        function() { displayGlobalDiv('vl-marc-upload-div'); });
-                                }
-                        }
-                        evt.target.selectedIndex = 0;
-                    }
-                </script>
-            </td>
-            <td align='middle' valign='bottom'>
-                <style type="text/css">.filter_span { padding-right: 5px; border-right: 2px solid #e8e1cf; } </style>
-                <table><tr>
-                    <td>
-                        <span>&vandelay.limit.to.collision.matches;</span>
-                        <span class='filter_span'>
-                            <input dojoType='dijit.form.CheckBox' jsId='vlQueueGridShowMatches' onchange='retrieveQueuedRecords();'/>
-                        </span>
-
-                        <span>&vandelay.limit.to.non.imported;</span>
-                        <span class='filter_span'>
-                            <input dojoType='dijit.form.CheckBox' jsId='vlQueueGridShowNonImport' 
-                                checked='checked' onchange='retrieveQueuedRecords();'/>
-                        </span>
-
-                        <span>&vandelay.results.per.page;</span>
-                        <span class='filter_span'>
+        <td align='right' valign='bottom'> <!-- big right td -->
+            <table id='vl-queue-paging-table' class='queue-nav-table'>
+                <tbody>
+                    <tr><td valign='bottom' align='right'>
+                        <select id='vl-queue-export-options' style='margin-right: 10px;'>
+                            <!-- TODO I18N -->
+                            <option value=''>Export Queue As...</option>
+                            <option value='print'>Print</option>
+                            <option value='csv'>CSV</option>
+                            <option value='email'>Email</option>
+                        </select>
+                        <span style='padding-right:5px;'>&vandelay.results.per.page;</span>
+                        <span class='queue-pager-span'>
                             <select jsId='vlQueueDisplayLimit' id='vl-queue-display-limit-selector'
                                 value='10' onchange='retrieveQueuedRecords();'>
                                 <option value='10'>10</option>
                             </select>
                         </span>
 
-                        <span style='padding-left:5px;'>&vandelay.page;</span>
-                        <input style='width:36px;' dojoType='dijit.form.TextBox' jsId='vlQueueDisplayPage' value='1'/>
-                    </td>
-                </tr></table>
-            </td>
-            <td align='right' valign='bottom'>
-                <span style='padding-right:4px;'>
-                    <a href='javascript:void(0);' onclick='vlQueueGridPrevPage();'>&vandelay.prev.page;</a>
-                </span>
-                <span style='padding-right:10px;'>
-                    <a href='javascript:void(0);' onclick='vlQueueGridNextPage();'>&vandelay.next.page;</a>
-                </span>
-            </td>
-        </tr>
-    </table>
-</div>
+                        <span class='queue-pager-span'>
+                            <span style='padding-left:5px;'>&vandelay.page;</span>
+                            <input style='width:36px;' dojoType='dijit.form.TextBox' jsId='vlQueueDisplayPage' value='1'/>
+                        </span>
 
+                        <span style='padding-right:8px;'>
+                            <a href='javascript:void(0);' onclick='vlQueueGridPrevPage();'>&vandelay.prev.page;</a>
+                        </span>
+                        <span style='padding-right:10px;'>
+                            <a href='javascript:void(0);' onclick='vlQueueGridNextPage();'>&vandelay.next.page;</a>
+                        </span>
+                    </td></tr>
+                </tbody>
+            </table>
+        </td>
+    </tr></table>
+    </fieldset>
+</div>
 
 <!-- Bib Queue Grid -->
 <div class='' id='vl-bib-queue-grid-wrapper' dojoType='dijit.layout.ContentPane'>
                     field='+row_selector'
                     get='vlQueueGridDrawSelectBox'
                     formatter='vlQueueGridFormatSelectBox'
-                    styles='text-align: center;'
+                    styles='text-align: center;width:30px;'
                     nonSelectable='true'>
                         <input id="vl-queue-grid-row-selector" type="checkbox" onclick="vlToggleQueueGridSelect();"></input>
                 </th>
                     styles='text-align: center;'
                     nonSelectable='true'>&vandelay.matches;</th>
                 <th
+                    field='+get_errors'
+                    get='vlGetViewErrors'
+                    formatter='vlFormatViewErrors'
+                    styles='text-align: center;'
+                    nonSelectable='true'>Import Failures</th>
+                <th
                     field='import_time'
                     styles='text-align: center;'
                     get='vlGetDateTimeField'>&vandelay.import.time;</th>
                     styles='text-align: center;'
                     nonSelectable='true'>&vandelay.matches;</th>
                 <th
+                    field='+get_errors'
+                    get='vlGetViewErrors'
+                    formatter='vlFormatViewErrors'
+                    styles='text-align: center;'
+                    nonSelectable='true'>Import Failures</th>
+                <th
                     field='import_time'
                     styles='text-align: center;'
                     get='vlGetDateTimeField'>&vandelay.import.time;</th>
     <div/>
 </div>
 
+<div jsId='queueItemsImportDialog' dojoType="dijit.Dialog" title="Import Items">
+    <div dojoType="dijit.layout.ContentPane">
+        <table class='form_table'>
+            <tbody>
+                <tr>
+                    <td>&vandelay.auto.import.merge_profile;</td>
+                    <td colspan='4'>
+                        <div jsId='vlUploadMergeProfile2' 
+                            dojoType='dijit.form.FilteringSelect' required='false' labelAttr='name' searchAttr='name'/>
+                    </td>
+                </tr>
+                <tr>
+                    <td>&vandelay.auto.import.noncolliding;</td>
+                    <td colspan='4'>
+                        <input jsId='vlUploadQueueImportNoMatch2' dojoType='dijit.form.CheckBox'/>
+                    </td>
+                </tr>
+                <tr>
+                    <td>&vandelay.auto.import.auto_overlay_exact;</td>
+                    <td colspan='4'>
+                        <input jsId='vlUploadQueueAutoOverlayExact2' dojoType='dijit.form.CheckBox'/>
+                    </td>
+                </tr>
+                <tr>
+                    <td>&vandelay.auto.import.auto_overlay_1match;</td>
+                    <td colspan='4'>
+                        <input jsId='vlUploadQueueAutoOverlay1Match2' dojoType='dijit.form.CheckBox'/>
+                    </td>
+                </tr>
+                <tr>
+                    <td>&vandelay.auto.import.auto_overlay_best;</td>
+                    <td colspan='4'><input jsId='vlUploadQueueAutoOverlayBestMatch2' dojoType='dijit.form.CheckBox'/></td>
+                </tr>
+                <tr>
+                    <td>&vandelay.auto.import.auto_overlay_best_ratio;</td>
+                    <td colspan='4'>
+                        <input style='width:3em' value='0.0' jsId='vlUploadQueueAutoOverlayBestMatchRatio2' dojoType='dijit.form.TextBox'/>
+                        <span style='padding-left: 10px; font-size:90%'>(&vandelay.auto.import.auto_overlay_best_ratio.desc;)</span>
+                    </td>
+                </tr>
+                <tr>
+                    <td>
+                        <button dojoType='dijit.form.Button' jsId='queueItemsImportCancelButton'>Cancel</button>
+                    </td>
+                    <td>
+                        <button dojoType='dijit.form.Button' jsId='queueItemsImportGoButton'>Import</button>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
+
 
index 69c2190..144cd45 100644 (file)
@@ -9,6 +9,8 @@
         onclick="vlShowAttrEditor();" showLabel="true">&vandelay.edit.attributes;</div>
     <div dojoType="dijit.form.Button" iconClass="dijitEditorIcon dijitEditorIconCopy"  id='vl-menu-profile-editor'
         onclick="vlShowProfileEditor();" showLabel="true">&vandelay.edit.profiles;</div>
+    <div dojoType="dijit.form.Button" iconClass="dijitEditorIcon dijitEditorIconCopy"  id='vl-menu-match-set-editor'
+        onclick="vlShowMatchSetEditor();" showLabel="true">&vandelay.edit.match_set;</div>
     <div dojoType="dijit.form.Button" iconClass="dijitEditorIcon dijitEditorIconCopy"  id='vl-menu-import-item-attr-editor'
         onclick="vlShowImportItemAttrEditor();" showLabel="true">&vandelay.edit.import_item_attrs;</div>
 </div>
index aca6320..f742104 100644 (file)
             </td>
         </tr>
         <tr>
-            <td>&vandelay.auto.import.noncolliding;</td>
-            <td colspan='4'>
-                <input jsId='vlUploadQueueAutoImport' dojoType='dijit.form.CheckBox'/>
+            <td>Record Match Set</td>
+            <td>
+                <input jsId='vlUploadQueueMatchSet'
+                    dojoType='dijit.form.FilteringSelect' labelAttr='name' searchAttr='name'/>
             </td>
         </tr>
         <tr>
-            <td>&vandelay.auto.import.auto_overlay_exact;</td>
-            <td colspan='4'>
-                <input jsId='vlUploadQueueAutoOverlayExact' dojoType='dijit.form.CheckBox'/>
+            <td>Holdings Import Profile</td>
+            <td>
+                <input jsId='vlUploadQueueHoldingsImportProfile'
+                    dojoType='dijit.form.FilteringSelect' labelAttr='name' searchAttr='name'/>
             </td>
         </tr>
         <tr>
-            <td>&vandelay.auto.import.auto_overlay_1match;</td>
-            <td colspan='4'>
-                <input jsId='vlUploadQueueAutoOverlay1Match' dojoType='dijit.form.CheckBox'/>
+            <td>&vandelay.import.bib_sources;</td>
+            <td>
+                <select name='bib_source' jsId='vlUploadSourceSelector' 
+                    dojoType='dijit.form.FilteringSelect' labelAttr='source' searchAttr='source'/>
             </td>
         </tr>
+        <tr><td colspan='2' style='margin-top:10px;border-bottom:1px solid #888;border-top:1px solid #888'><b>Import Actions</b></td></tr>
         <tr>
             <td>&vandelay.auto.import.merge_profile;</td>
             <td colspan='4'>
             </td>
         </tr>
         <tr>
-            <td>Holdings Import Profile</td>
-            <td>
-                <input jsId='vlUploadQueueHoldingsImportProfile'
-                    dojoType='dijit.form.FilteringSelect' labelAttr='name' searchAttr='name'/>
+            <td>&vandelay.auto.import.noncolliding;</td>
+            <td colspan='4'>
+                <input jsId='vlUploadQueueImportNoMatch' dojoType='dijit.form.CheckBox'/>
             </td>
         </tr>
         <tr>
-            <td>&vandelay.import.bib_sources;</td>
-            <td>
-                <select name='bib_source' jsId='vlUploadSourceSelector' 
-                    dojoType='dijit.form.FilteringSelect' labelAttr='source' searchAttr='source'/>
+            <td>&vandelay.auto.import.auto_overlay_exact;</td>
+            <td colspan='4'>
+                <input jsId='vlUploadQueueAutoOverlayExact' dojoType='dijit.form.CheckBox'/>
             </td>
         </tr>
         <tr>
-            <td>
-                <span id="vl-file-label">&vandelay.file.to.upload;</span>
+            <td>&vandelay.auto.import.auto_overlay_1match;</td>
+            <td colspan='4'>
+                <input jsId='vlUploadQueueAutoOverlay1Match' dojoType='dijit.form.CheckBox'/>
             </td>
-            <td id='vl-input-td' colspan='4'>
-                <input size='48' type="file" name="marc_upload"/>
+        </tr>
+        <tr>
+            <td>&vandelay.auto.import.auto_overlay_best;</td>
+            <td colspan='4'><input jsId='vlUploadQueueAutoOverlayBestMatch' dojoType='dijit.form.CheckBox'/></td>
+        </tr>
+        <tr>
+            <td>&vandelay.auto.import.auto_overlay_best_ratio;</td>
+            <td colspan='4'>
+                <input style='width:3em' value='0.0' jsId='vlUploadQueueAutoOverlayBestMatchRatio' dojoType='dijit.form.TextBox'/>
+                <span style='padding-left: 10px; font-size:90%'>(&vandelay.auto.import.auto_overlay_best_ratio.desc;)</span>
             </td>
         </tr>
+
+        <tr><td colspan='2' style='border-bottom:1px solid #888'></td></tr>
         <tr>
-            <td align='center' colspan='4'>
-                <button dojoType="dijit.form.Button" onclick="batchUpload()">&vandelay.upload;</button>
+            <td colspan='5'>
+                <span id="vl-file-label">&vandelay.file.to.upload;</span>
+                <input size='48' type="file" name="marc_upload"/>
+                <span style='margin-left:10px;'><button dojoType="dijit.form.Button" onclick="batchUpload()">&vandelay.upload;</button></span>
             </td>
         </tr>
     </table>
index 2e2a2e0..78f23a8 100644 (file)
 <div dojoType="dijit.layout.ContentPane" layoutAlign='client' id='vl-profile-editor-div' class='hidden content'>
     [% INCLUDE 'default/vandelay/inc/profiles.tt2' %]
 </div>
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client' id='vl-match-set-editor-div' class='hidden content'>
+</div>
 <div dojoType="dijit.layout.ContentPane" layoutAlign='client' id='vl-item-attr-editor-div' class='hidden content'>
     [% INCLUDE 'default/vandelay/inc/item_attrs.tt2' %]
 </div>
+<div dojoType="dijit.layout.ContentPane" layoutAlign='client' id='vl-import-error-div' class='hidden content'>
+    [% INCLUDE 'default/vandelay/inc/import_errors.tt2' %]
+</div>
 
 
 [% END %]
index bc8c11d..48636f0 100644 (file)
@@ -932,6 +932,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/org_unit_setting_type', null, event); }
             ],
+            'cmd_server_admin_import_match_set' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/vandelay/match_set', null, event); }
+            ],
             'cmd_server_admin_usr_setting_type' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/usr_setting_type', null, event); }
index 2dc7cb5..0b358ec 100644 (file)
              perm="ADMIN_GLOBAL_FLAG"
              />
     <command id="cmd_server_admin_org_unit_setting_type" />
+    <command id="cmd_server_admin_import_match_set" />
     <command id="cmd_server_admin_usr_setting_type" />
     <command id="cmd_server_admin_config_hard_due_date"
              perm="CREATE_CIRC_DURATION DELETE_CIRC_DURATION UPDATE_CIRC_DURATION"
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.z3950_source.label;" command="cmd_server_admin_z39_source"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.circulation_modifier.label;" command="cmd_server_admin_circ_mod"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.global_flag.label;" command="cmd_server_admin_global_flag"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.import_match_set;" command="cmd_server_admin_import_match_set"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.org_unit_setting_type;" command="cmd_server_admin_org_unit_setting_type"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.usr_setting_type;" command="cmd_server_admin_usr_setting_type"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_hard_due_date;" command="cmd_server_admin_config_hard_due_date"/>