Patch from Thomas Berezansky addressing logical importance of various circulation...
authormiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 28 Jan 2011 14:58:37 +0000 (14:58 +0000)
committermiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 28 Jan 2011 14:58:37 +0000 (14:58 +0000)
Previous to this commit, INDB circ and holds use a pre-defined weighting set for rule ordering. This can be changed via replacing the relevant "find" functions in the database, but this is not easily done for most people.

The weight set for circ matchpoints is obtained based on the context ou of the circ (aka, where the circ is happening).

The weight set for hold matchpoints is obtained based on the item's circ library (aka, where the item lives).

Optionally, add an enabled circ.holds.weight_owner_not_circ internal flag to have the weight set for hold matchpoints be obtained based on the item's owning library (owner of the call number).

TODO: discuss promotion of circ.holds.weight_owner_not_circ to a Global Flag; wikified or docbook'd documentation and use-case examples.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@19317 dcc99617-32d9-48b4-a31d-7c20da2025e4

16 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/sql/Pg/006.schema.permissions.sql
Open-ILS/src/sql/Pg/020.schema.functions.sql
Open-ILS/src/sql/Pg/099.matrix_weights.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/110.hold_matrix.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/build-db.sh
Open-ILS/src/sql/Pg/upgrade/0479.schema.matrix_weights.sql [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/config/hold_matrix_matchpoint.tt2
Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2 [new file with mode: 0644]
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul

index 4c551a7..a68a9d5 100644 (file)
@@ -1003,6 +1003,85 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         </permacrud>
        </class>
 
+    <class id="chmw" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::hold_matrix_weights" oils_persist:tablename="config.hold_matrix_weights" reporter:label="Hold Matrix Weights">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.hold_matrix_weights_id_seq">
+            <field reporter:label="Hold Weights ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+            <field reporter:label="Name" name="name" reporter:datatype="text"/>
+            <field reporter:label="User Home Library" name="user_home_ou" reporter:datatype="float"/>
+            <field reporter:label="Request Library" name="request_ou" reporter:datatype="float"/>
+            <field reporter:label="Pickup Library" name="pickup_ou" reporter:datatype="float"/>
+            <field reporter:label="Owning Library" name="item_owning_ou" reporter:datatype="float"/>
+            <field reporter:label="Item Circ Library" name="item_circ_ou" reporter:datatype="float"/>
+            <field reporter:label="User Permission Group" name="usr_grp" reporter:datatype="float"/>
+            <field reporter:label="Requestor Permission Group" name="requestor_grp" reporter:datatype="float"/>
+            <field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="float"/>
+            <field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="float"/>
+            <field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="float"/>
+            <field reporter:label="Videorecording Format" name="marc_vr_format" oils_persist:primitive="string" reporter:datatype="float"/>
+            <field reporter:label="Reference?" name="ref_flag" reporter:datatype="float"/>
+        </fields>
+        <links/>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_HOLD_MATRIX_MATCHPOINT" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_HOLD_MATRIX_MATCHPOINT" global_required="true"/>
+                <delete permission="ADMIN_HOLD_MATRIX_MATCHPOINT" global_required="true"/>
+            </actions>
+        </permacrud>
+    </class>
+
+    <class id="ccmw" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::circ_matrix_weights" oils_persist:tablename="config.circ_matrix_weights" reporter:label="Circ Matrix Weights">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.circ_matrix_weights_id_seq">
+            <field reporter:label="Circ Weights ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+            <field reporter:label="Name" name="name" reporter:datatype="text"/>
+            <field reporter:label="Org Unit" name="org_unit" reporter:datatype="float"/>
+            <field reporter:label="Copy Circ Lib" name="copy_circ_lib" reporter:datatype="float"/>
+            <field reporter:label="Copy Owning Lib" name="copy_owning_lib" reporter:datatype="float"/>
+            <field reporter:label="User Home Lib" name="user_home_ou" reporter:datatype="float"/>
+            <field reporter:label="Permission Group" name="grp" reporter:datatype="float"/>
+            <field reporter:label="Circulation Modifier" name="circ_modifier" reporter:datatype="float"/>
+            <field reporter:label="MARC Type" name="marc_type" reporter:datatype="float"/>
+            <field reporter:label="MARC Form" name="marc_form" reporter:datatype="float"/>
+            <field reporter:label="Videorecording Format" name="marc_vr_format" reporter:datatype="float"/>
+            <field reporter:label="Reference?" name="ref_flag" reporter:datatype="float"/>
+            <field reporter:label="User Age: Lower Bound" name="usr_age_lower_bound" reporter:datatype="float"/>
+            <field reporter:label="User Age: Upper Bound" name="usr_age_upper_bound" reporter:datatype="float"/>
+        </fields>
+        <links/>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+                <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT" global_required="true"/>
+            </actions>
+        </permacrud>
+    </class>
+
+    <class id="cwa" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::weight_assoc" oils_persist:tablename="config.weight_assoc" reporter:label="Matrix Weight Association">
+        <fields oils_persist:primary="id" oils_persist:sequence="config.weight_assoc_id_seq">
+            <field reporter:label="Assoc ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Active?" name="active" reporter:datatype="bool"/>
+            <field reporter:label="Org Unit" name="org_unit" reporter:datatype="org_unit"/>
+            <field reporter:label="Circ Weights" name="circ_weights" reporter:datatype="link"/>
+            <field reporter:label="Hold Weights" name="hold_weights" reporter:datatype="link"/>
+        </fields>
+        <links>
+            <link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
+            <link field="circ_weights" reltype="has_a" key="id" map="" class="ccmw"/>
+            <link field="hold_weights" reltype="has_a" key="id" map="" class="chmw"/>
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_CIRC_MATRIX_MATCHPOINT ADMIN_HOLD_MATRIX_MATCHPOINT" context_field='org_unit'/>
+                <retrieve permission="ADMIN_CIRC_MATRIX_MATCHPOINT ADMIN_HOLD_MATRIX_MATCHPOINT VIEW_CIRC_MATRIX_MATCHPOINT VIEW_HOLD_MATRIX_MATCHPOINT" context_field='org_unit'/>
+                <update permission="ADMIN_CIRC_MATRIX_MATCHPOINT ADMIN_HOLD_MATRIX_MATCHPOINT" context_field='org_unit'/>
+                <delete permission="ADMIN_CIRC_MATRIX_MATCHPOINT ADMIN_HOLD_MATRIX_MATCHPOINT" context_field='org_unit'/>
+            </actions>
+        </permacrud>
+    </class>
+
        <class id="chmm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::hold_matrix_matchpoint" oils_persist:tablename="config.hold_matrix_matchpoint" reporter:label="Hold Matrix Matchpoint">
                <fields oils_persist:primary="id" oils_persist:sequence="config.hold_matrix_matchpoint_id_seq">
                        <field reporter:label="Matchpoint ID" name="id" reporter:datatype="id"/>
@@ -1060,6 +1139,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Org Unit" name="org_unit" reporter:datatype="org_unit" oils_obj:required="true"/>
                        <field reporter:label="Copy Circ Lib" name="copy_circ_lib" reporter:datatype="org_unit"/>
                        <field reporter:label="Copy Owning Lib" name="copy_owning_lib" reporter:datatype="org_unit"/>
+                       <field reporter:label="User Home Lib" name="user_home_ou" reporter:datatype="org_unit"/>
                        <field reporter:label="Permission Group" name="grp" reporter:datatype="link" oils_obj:required="true"/>
                        <field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="link"/>
@@ -1081,6 +1161,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="copy_circ_lib" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="copy_owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="user_home_ou" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="grp" reltype="has_a" key="id" map="" class="pgt"/>
                        <link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
                        <link field="marc_type" reltype="has_a" key="code" map="" class="citm"/>
index 4be5c7d..baf2777 100644 (file)
@@ -101,6 +101,27 @@ CREATE OR REPLACE FUNCTION permission.grp_ancestors ( INT ) RETURNS SETOF permis
                END, a.name;
 $$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.parent, gad.distance+1
+            FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON (pgt.id = gad.id)
+            WHERE pgt.parent IS NOT NULL
+    )
+    SELECT * FROM grp_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.id, gdd.distance+1
+            FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON (pgt.parent = gdd.id)
+    )
+    SELECT * FROM grp_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
 CREATE OR REPLACE FUNCTION permission.usr_perms ( INT ) RETURNS SETOF permission.usr_perm_map AS $$
        SELECT  DISTINCT ON (usr,perm) *
          FROM  (
index 1745dbb..df3ef90 100644 (file)
@@ -224,6 +224,16 @@ CREATE OR REPLACE FUNCTION actor.org_unit_descendants( INT ) RETURNS SETOF actor
     ) SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id);
 $$ LANGUAGE SQL;
 
+CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.id, oudd.distance+1
+            FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON (ou.parent_ou = oudd.id)
+    )
+    SELECT * FROM org_unit_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors( INT ) RETURNS SETOF actor.org_unit AS $$
     WITH RECURSIVE anscestor_depth AS (
         SELECT  ou.id,
@@ -247,6 +257,17 @@ CREATE OR REPLACE FUNCTION actor.org_unit_ancestor_at_depth ( INT,INT ) RETURNS
                                        ON x.ou_type = y.id AND y.depth = $2);
 $$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.parent_ou, ouad.distance+1
+            FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON (ou.id = ouad.id)
+            WHERE ou.parent_ou IS NOT NULL
+    )
+    SELECT * FROM org_unit_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
 CREATE OR REPLACE FUNCTION actor.org_unit_full_path ( INT ) RETURNS SETOF actor.org_unit AS $$
        SELECT  *
          FROM  actor.org_unit_ancestors($1)
diff --git a/Open-ILS/src/sql/Pg/099.matrix_weights.sql b/Open-ILS/src/sql/Pg/099.matrix_weights.sql
new file mode 100644 (file)
index 0000000..521171c
--- /dev/null
@@ -0,0 +1,53 @@
+
+BEGIN;
+
+-- Circ Matrix Weights
+CREATE TABLE config.circ_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    org_unit                NUMERIC(6,2)   NOT NULL,
+    grp                     NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    copy_circ_lib           NUMERIC(6,2)   NOT NULL,
+    copy_owning_lib         NUMERIC(6,2)   NOT NULL,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    is_renewal              NUMERIC(6,2)   NOT NULL,
+    usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
+    usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
+);
+
+-- Hold Matrix Weights
+CREATE TABLE config.hold_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    request_ou              NUMERIC(6,2)   NOT NULL,
+    pickup_ou               NUMERIC(6,2)   NOT NULL,
+    item_owning_ou          NUMERIC(6,2)   NOT NULL,
+    item_circ_ou            NUMERIC(6,2)   NOT NULL,
+    usr_grp                 NUMERIC(6,2)   NOT NULL,
+    requestor_grp           NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL
+);
+
+-- Linking between weights and org units
+CREATE TABLE config.weight_assoc (
+    id                      SERIAL  PRIMARY KEY,
+    active                  BOOL    NOT NULL,
+    org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+);
+CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
+
+COMMIT;
index 1b05604..c16adaf 100644 (file)
@@ -76,20 +76,6 @@ INSERT INTO config.videorecording_format_map VALUES ('z','Other');
  **  developers focus on specific parts of the matrix.
  **/
 
-
---
---                 ****** Which ruleset and tests to use *******
---
--- * Most specific range for org_unit and grp wins.
---
--- * circ_modifier match takes precidence over marc_type match, if circ_modifier is set here
---
--- * marc_type is first checked against the circ_as_type from the copy, then the item type from the marc record
---
--- * If neither circ_modifier nor marc_type is set (both are NULLABLE) then the entry defines the default
---   ruleset and tests for the OU + group (like BOOK in PINES)
---
-
 CREATE TABLE config.circ_matrix_matchpoint (
     id                   SERIAL    PRIMARY KEY,
     active               BOOL    NOT NULL DEFAULT TRUE,
@@ -101,6 +87,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
     marc_vr_format       TEXT    REFERENCES config.videorecording_format_map (code) DEFERRABLE INITIALLY DEFERRED,
     copy_circ_lib        INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
     copy_owning_lib      INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
+    user_home_ou         INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
     ref_flag             BOOL,
     juvenile_flag        BOOL,
     is_renewal           BOOL,
@@ -138,102 +125,116 @@ CREATE TABLE config.circ_matrix_circ_mod_test_map (
 
 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS config.circ_matrix_matchpoint AS $func$
 DECLARE
-    current_group    permission.grp_tree%ROWTYPE;
-    user_object    actor.usr%ROWTYPE;
-    item_object    asset.copy%ROWTYPE;
-    cn_object    asset.call_number%ROWTYPE;
-    rec_descriptor    metabib.rec_descriptor%ROWTYPE;
-    current_mp    config.circ_matrix_matchpoint%ROWTYPE;
-    matchpoint    config.circ_matrix_matchpoint%ROWTYPE;
+    user_object     actor.usr%ROWTYPE;
+    item_object     asset.copy%ROWTYPE;
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    denominator     INT;
 BEGIN
-    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
-    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
-    SELECT INTO cn_object * FROM asset.call_number WHERE id = item_object.call_number;
-    SELECT INTO rec_descriptor r.* FROM metabib.rec_descriptor r JOIN asset.call_number c USING (record) WHERE c.id = item_object.call_number;
-    SELECT INTO current_group * FROM permission.grp_tree WHERE id = user_object.profile;
-
-    LOOP 
-        -- for each potential matchpoint for this ou and group ...
-        FOR current_mp IN
-            SELECT  m.*
-              FROM  config.circ_matrix_matchpoint m
-                    JOIN actor.org_unit_ancestors( context_ou ) d ON (m.org_unit = d.id)
-                    LEFT JOIN actor.org_unit_proximity p ON (p.from_org = context_ou AND p.to_org = d.id)
-              WHERE m.grp = current_group.id
-                    AND m.active
-                    AND (m.copy_owning_lib IS NULL OR cn_object.owning_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_owning_lib) ))
-                    AND (m.copy_circ_lib   IS NULL OR item_object.circ_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_circ_lib)   ))
-              ORDER BY    CASE WHEN p.prox        IS NULL THEN 999 ELSE p.prox END,
-                    CASE WHEN m.copy_owning_lib IS NOT NULL
-                        THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = cn_object.owning_lib AND from_org = m.copy_owning_lib LIMIT 1 )
-                        ELSE 0
-                    END +
-                    CASE WHEN m.copy_circ_lib IS NOT NULL
-                        THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = item_object.circ_lib AND from_org = m.copy_circ_lib LIMIT 1 )
-                        ELSE 0
-                    END +
-                    CASE WHEN m.is_renewal = renewal        THEN 128 ELSE 0 END +
-                    CASE WHEN m.juvenile_flag    IS NOT NULL THEN 64 ELSE 0 END +
-                    CASE WHEN m.circ_modifier    IS NOT NULL THEN 32 ELSE 0 END +
-                    CASE WHEN m.marc_type        IS NOT NULL THEN 16 ELSE 0 END +
-                    CASE WHEN m.marc_form        IS NOT NULL THEN 8 ELSE 0 END +
-                    CASE WHEN m.marc_vr_format    IS NOT NULL THEN 4 ELSE 0 END +
-                    CASE WHEN m.ref_flag        IS NOT NULL THEN 2 ELSE 0 END +
-                    CASE WHEN m.usr_age_lower_bound    IS NOT NULL THEN 0.5 ELSE 0 END +
-                    CASE WHEN m.usr_age_upper_bound    IS NOT NULL THEN 0.5 ELSE 0 END DESC LOOP
-
-            IF current_mp.is_renewal IS NOT NULL THEN
-                CONTINUE WHEN current_mp.is_renewal <> renewal;
-            END IF;
-
-            IF current_mp.circ_modifier IS NOT NULL THEN
-                CONTINUE WHEN current_mp.circ_modifier <> item_object.circ_modifier OR item_object.circ_modifier IS NULL;
-            END IF;
-
-            IF current_mp.marc_type IS NOT NULL THEN
-                IF item_object.circ_as_type IS NOT NULL THEN
-                    CONTINUE WHEN current_mp.marc_type <> item_object.circ_as_type;
-                ELSE
-                    CONTINUE WHEN current_mp.marc_type <> rec_descriptor.item_type;
-                END IF;
-            END IF;
-
-            IF current_mp.marc_form IS NOT NULL THEN
-                CONTINUE WHEN current_mp.marc_form <> rec_descriptor.item_form;
-            END IF;
-
-            IF current_mp.marc_vr_format IS NOT NULL THEN
-                CONTINUE WHEN current_mp.marc_vr_format <> rec_descriptor.vr_format;
-            END IF;
-
-            IF current_mp.ref_flag IS NOT NULL THEN
-                CONTINUE WHEN current_mp.ref_flag <> item_object.ref;
-            END IF;
-
-            IF current_mp.juvenile_flag IS NOT NULL THEN
-                CONTINUE WHEN current_mp.juvenile_flag <> user_object.juvenile;
-            END IF;
-
-            IF current_mp.usr_age_lower_bound IS NOT NULL THEN
-                CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_lower_bound < age(user_object.dob);
-            END IF;
-
-            IF current_mp.usr_age_upper_bound IS NOT NULL THEN
-                CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_upper_bound > age(user_object.dob);
-            END IF;
-
-
-            -- everything was undefined or matched
-            matchpoint = current_mp;
-
-            EXIT WHEN matchpoint.id IS NOT NULL;
-        END LOOP;
-
-        EXIT WHEN current_group.parent IS NULL OR matchpoint.id IS NOT NULL;
+    SELECT INTO user_object     * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO item_object     * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
 
-        SELECT INTO current_group * FROM permission.grp_tree WHERE id = current_group.parent;
-    END LOOP;
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11;
+        weights.org_unit            := 10;
+        weights.circ_modifier       := 5;
+        weights.marc_type           := 4;
+        weights.marc_form           := 3;
+        weights.marc_vr_format      := 2;
+        weights.copy_circ_lib       := 8;
+        weights.copy_owning_lib     := 8;
+        weights.user_home_ou        := 8;
+        weights.ref_flag            := 1;
+        weights.juvenile_flag       := 6;
+        weights.is_renewal          := 7;
+        weights.usr_age_lower_bound := 0;
+        weights.usr_age_upper_bound := 0;
+    END IF;
 
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+                   SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.circ_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+            /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+            -- Org Units
+         -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+            AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+            AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+            AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+            -- Circ Type
+            AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+            -- Static User Checks
+            AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+            AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+            AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+            -- Static Item Checks
+            AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0 END +
+            -- Org Units
+            CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0 END +
+            CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
+            -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0 END +
+            -- Static User Checks
+            CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
+            CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0 END +
+            CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
+            CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
+            CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
+            CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
+            CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return the entire matchpoint
     RETURN matchpoint;
 END;
 $func$ LANGUAGE plpgsql;
index de2eb4f..84c6b11 100644 (file)
@@ -55,155 +55,146 @@ CREATE TABLE config.hold_matrix_matchpoint (
     CONSTRAINT hous_once_per_grp_loc_mod_marc UNIQUE (user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, requestor_grp, usr_grp, circ_modifier, marc_type, marc_form, marc_vr_format, ref_flag, juvenile_flag)
 );
 
-CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT ) RETURNS INT AS $func$
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+  RETURNS integer AS
+$func$
 DECLARE
-    current_requestor_group    permission.grp_tree%ROWTYPE;
     requestor_object    actor.usr%ROWTYPE;
-    user_object        actor.usr%ROWTYPE;
-    item_object        asset.copy%ROWTYPE;
-    item_cn_object        asset.call_number%ROWTYPE;
-    rec_descriptor        metabib.rec_descriptor%ROWTYPE;
-    current_mp_weight    FLOAT;
-    matchpoint_weight    FLOAT;
-    tmp_weight        FLOAT;
-    current_mp        config.hold_matrix_matchpoint%ROWTYPE;
-    matchpoint        config.hold_matrix_matchpoint%ROWTYPE;
+    user_object         actor.usr%ROWTYPE;
+    item_object         asset.copy%ROWTYPE;
+    item_cn_object      asset.call_number%ROWTYPE;
+    rec_descriptor      metabib.rec_descriptor%ROWTYPE;
+    matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
+    weights             config.hold_matrix_weights%ROWTYPE;
+    denominator         INT;
 BEGIN
-    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
-    SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor;
-    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
-    SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
-    SELECT INTO rec_descriptor r.* FROM metabib.rec_descriptor r WHERE r.record = item_cn_object.record;
-
-    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
-
+    SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
+    SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
+
+    -- The item's owner should probably be the one determining if the item is holdable
+    -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+    -- This flag will allow for setting it to the owning library (where the call number "lives")
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+    -- Grab the closest set circ weight setting.
     IF NOT FOUND THEN
-        SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = requestor_object.profile;
+        -- Default to circ library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
     ELSE
-        SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = user_object.profile;
+        -- Flag is set, use owning library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
     END IF;
 
-    LOOP 
-        -- for each potential matchpoint for this ou and group ...
-        FOR current_mp IN
-            SELECT    m.*
-              FROM    config.hold_matrix_matchpoint m
-              WHERE    m.requestor_grp = current_requestor_group.id AND m.active
-              ORDER BY    CASE WHEN m.circ_modifier    IS NOT NULL THEN 16 ELSE 0 END +
-                    CASE WHEN m.juvenile_flag    IS NOT NULL THEN 16 ELSE 0 END +
-                    CASE WHEN m.marc_type        IS NOT NULL THEN 8 ELSE 0 END +
-                    CASE WHEN m.marc_form        IS NOT NULL THEN 4 ELSE 0 END +
-                    CASE WHEN m.marc_vr_format    IS NOT NULL THEN 2 ELSE 0 END +
-                    CASE WHEN m.ref_flag        IS NOT NULL THEN 1 ELSE 0 END DESC LOOP
-
-            IF NOT current_mp.strict_ou_match THEN
-                current_mp_weight := 5.0;
-            ELSE
-                current_mp_weight := 0.0;
-            END IF;
-
-            IF current_mp.circ_modifier IS NOT NULL THEN
-                CONTINUE WHEN current_mp.circ_modifier <> item_object.circ_modifier OR item_object.circ_modifier IS NULL;
-            END IF;
-
-            IF current_mp.marc_type IS NOT NULL THEN
-                IF item_object.circ_as_type IS NOT NULL THEN
-                    CONTINUE WHEN current_mp.marc_type <> item_object.circ_as_type;
-                ELSE
-                    CONTINUE WHEN current_mp.marc_type <> rec_descriptor.item_type;
-                END IF;
-            END IF;
-
-            IF current_mp.marc_form IS NOT NULL THEN
-                CONTINUE WHEN current_mp.marc_form <> rec_descriptor.item_form;
-            END IF;
-
-            IF current_mp.marc_vr_format IS NOT NULL THEN
-                CONTINUE WHEN current_mp.marc_vr_format <> rec_descriptor.vr_format;
-            END IF;
-
-            IF current_mp.juvenile_flag IS NOT NULL THEN
-                CONTINUE WHEN current_mp.juvenile_flag <> user_object.juvenile;
-            END IF;
-
-            IF current_mp.ref_flag IS NOT NULL THEN
-                CONTINUE WHEN current_mp.ref_flag <> item_object.ref;
-            END IF;
-
-
-            -- caclulate the rule match weight
-            IF current_mp.item_owning_ou IS NOT NULL THEN
-                CONTINUE WHEN current_mp.item_owning_ou NOT IN (SELECT (actor.org_unit_ancestors(item_cn_object.owning_lib)).id);
-                IF NOT current_mp.strict_ou_match THEN
-                    SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.item_owning_ou, item_cn_object.owning_lib)::FLOAT + 1.0)::FLOAT;
-                ELSE
-                    CONTINUE WHEN current_mp.item_owning_ou <> item_cn_object.owning_lib;
-                    tmp_weight := CASE WHEN current_mp.item_owning_ou = item_cn_object.owning_lib THEN 1.0 ELSE 0.0 END;
-                END IF;
-                current_mp_weight := current_mp_weight - tmp_weight;
-            END IF; 
-
-            IF current_mp.item_circ_ou IS NOT NULL THEN
-                CONTINUE WHEN current_mp.item_circ_ou NOT IN (SELECT (actor.org_unit_ancestors(item_object.circ_lib)).id);
-                IF NOT current_mp.strict_ou_match THEN
-                    SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.item_circ_ou, item_object.circ_lib)::FLOAT + 1.0)::FLOAT;
-                ELSE
-                    CONTINUE WHEN current_mp.item_circ_ou <> item_object.circ_lib;
-                    tmp_weight := CASE WHEN current_mp.item_circ_ou = item_object.circ_lib THEN 1.0 ELSE 0.0 END;
-                END IF;
-                current_mp_weight := current_mp_weight - tmp_weight;
-            END IF; 
-
-            IF current_mp.pickup_ou IS NOT NULL THEN
-                CONTINUE WHEN current_mp.pickup_ou NOT IN (SELECT (actor.org_unit_ancestors(pickup_ou)).id);
-                IF NOT current_mp.strict_ou_match THEN
-                    SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.pickup_ou, pickup_ou)::FLOAT + 1.0)::FLOAT;
-                ELSE
-                    CONTINUE WHEN current_mp.pickup_ou <> pickup_ou;
-                    tmp_weight := CASE WHEN current_mp.pickup_ou = pickiup_ou THEN 1.0 ELSE 0.0 END;
-                END IF;
-                current_mp_weight := current_mp_weight - tmp_weight;
-            END IF; 
-
-            IF current_mp.request_ou IS NOT NULL THEN
-                CONTINUE WHEN current_mp.request_ou NOT IN (SELECT (actor.org_unit_ancestors(request_ou)).id);
-                IF NOT current_mp.strict_ou_match THEN
-                    SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.request_ou, request_ou)::FLOAT + 1.0)::FLOAT;
-                ELSE
-                    CONTINUE WHEN current_mp.request_ou <> request_ou;
-                    tmp_weight := CASE WHEN current_mp.request_ou = request_ou THEN 1.0 ELSE 0.0 END;
-                END IF;
-                current_mp_weight := current_mp_weight - tmp_weight;
-            END IF; 
-
-            IF current_mp.user_home_ou IS NOT NULL THEN
-                CONTINUE WHEN current_mp.user_home_ou NOT IN (SELECT (actor.org_unit_ancestors(user_object.home_ou)).id);
-                IF NOT current_mp.strict_ou_match THEN
-                    SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.user_home_ou, user_object.home_ou)::FLOAT + 1.0)::FLOAT;
-                ELSE
-                    CONTINUE WHEN current_mp.user_home_ou <> user_object.home_ou;
-                    tmp_weight := CASE WHEN current_mp.user_home_ou = user_object.home_ou THEN 1.0 ELSE 0.0 END;
-                END IF;
-                current_mp_weight := current_mp_weight - tmp_weight;
-            END IF; 
-
-            -- set the matchpoint if we found the best one
-            IF matchpoint_weight IS NULL OR matchpoint_weight > current_mp_weight THEN
-                matchpoint = current_mp;
-                matchpoint_weight = current_mp_weight;
-            END IF;
-
-        END LOOP;
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.user_home_ou    := 5;
+        weights.request_ou      := 5;
+        weights.pickup_ou       := 5;
+        weights.item_owning_ou  := 5;
+        weights.item_circ_ou    := 5;
+        weights.usr_grp         := 7;
+        weights.requestor_grp   := 8;
+        weights.circ_modifier   := 4;
+        weights.marc_type       := 3;
+        weights.marc_form       := 2;
+        weights.marc_vr_format  := 1;
+        weights.juvenile_flag   := 4;
+        weights.ref_flag        := 0;
+    END IF;
 
-        EXIT WHEN current_requestor_group.parent IS NULL OR matchpoint.id IS NOT NULL;
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+            SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+    -- This may be better implemented as part of the upgrade script?
+    -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+    -- Then remove this flag, of course.
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
 
-        SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = current_requestor_group.parent;
-    END LOOP;
+    IF FOUND THEN
+        -- Note: This, to me, is REALLY hacky. I put it in anyway.
+        -- If you can't tell, this is a single call swap on two variables.
+        SELECT INTO user_object.profile, requestor_object.profile
+                    requestor_object.profile, user_object.profile;
+    END IF;
 
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.hold_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+            LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+            LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+            AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
+            -- Org Units
+            AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+            -- Static User Checks
+            AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
+            -- Static Item Checks
+            AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0 END +
+            CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0 END +
+            -- Org Units
+            CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0 END +
+            CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
+            -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
+            CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
+            CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
+            CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return just the ID for now
     RETURN matchpoint.id;
 END;
-$func$ LANGUAGE plpgsql;
-
+$func$ LANGUAGE 'plpgsql';
 
 CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
 DECLARE
index 3b0618a..4b5f89a 100644 (file)
@@ -1621,10 +1621,24 @@ INSERT INTO asset.call_number VALUES (-1,1,NOW(),1,NOW(),-1,1,'UNCATALOGED');
 -- circ matrix
 INSERT INTO config.circ_matrix_matchpoint (org_unit,grp,duration_rule,recurring_fine_rule,max_fine_rule) VALUES (1,1,11,1,1);
 
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- hold matrix - 110.hold_matrix.sql:
 INSERT INTO config.hold_matrix_matchpoint (requestor_grp) VALUES (1);
 
+INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
+    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+
+-- dynamic weight associations
+INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
+    (true, 1, 1, 1);
 
 -- User setting types
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
index c53cef9..c496f75 100755 (executable)
@@ -101,7 +101,8 @@ ordered_file_list="
   080.schema.money.sql
   090.schema.action.sql
   095.schema.booking.sql
-  
+  099.matrix_weights.sql 
   100.circ_matrix.sql
   110.hold_matrix.sql
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/0479.schema.matrix_weights.sql b/Open-ILS/src/sql/Pg/upgrade/0479.schema.matrix_weights.sql
new file mode 100644 (file)
index 0000000..eb967f1
--- /dev/null
@@ -0,0 +1,368 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0XXX');
+
+CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.parent, gad.distance+1
+            FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
+            WHERE pgt.parent IS NOT NULL
+    )
+    SELECT * FROM grp_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.id, gdd.distance+1
+            FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
+    )
+    SELECT * FROM grp_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.parent_ou, ouad.distance+1
+            FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
+            WHERE ou.parent_ou IS NOT NULL
+    )
+    SELECT * FROM org_unit_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.id, oudd.distance+1
+            FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
+    )
+    SELECT * FROM org_unit_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN user_home_ou         INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE config.circ_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    org_unit                NUMERIC(6,2)   NOT NULL,
+    grp                     NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    copy_circ_lib           NUMERIC(6,2)   NOT NULL,
+    copy_owning_lib         NUMERIC(6,2)   NOT NULL,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    is_renewal              NUMERIC(6,2)   NOT NULL,
+    usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
+    usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
+);
+
+CREATE TABLE config.hold_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    request_ou              NUMERIC(6,2)   NOT NULL,
+    pickup_ou               NUMERIC(6,2)   NOT NULL,
+    item_owning_ou          NUMERIC(6,2)   NOT NULL,
+    item_circ_ou            NUMERIC(6,2)   NOT NULL,
+    usr_grp                 NUMERIC(6,2)   NOT NULL,
+    requestor_grp           NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL
+);
+
+CREATE TABLE config.weight_assoc (
+    id                      SERIAL  PRIMARY KEY,
+    active                  BOOL    NOT NULL,
+    org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+);
+CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
+
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS config.circ_matrix_matchpoint AS $func$
+DECLARE
+    user_object     actor.usr%ROWTYPE;
+    item_object     asset.copy%ROWTYPE;
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    denominator     INT;
+BEGIN
+    SELECT INTO user_object     * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO item_object     * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
+
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11;
+        weights.org_unit            := 10;
+        weights.circ_modifier       := 5;
+        weights.marc_type           := 4;
+        weights.marc_form           := 3;
+        weights.marc_vr_format      := 2;
+        weights.copy_circ_lib       := 8;
+        weights.copy_owning_lib     := 8;
+        weights.user_home_ou        := 8;
+        weights.ref_flag            := 1;
+        weights.juvenile_flag       := 6;
+        weights.is_renewal          := 7;
+        weights.usr_age_lower_bound := 0;
+        weights.usr_age_upper_bound := 0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+                   SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.circ_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+            /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+            -- Org Units
+         -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+            AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+            AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+            AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+            -- Circ Type
+            AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+            -- Static User Checks
+            AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+            AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+            AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+            -- Static Item Checks
+            AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0 END +
+            -- Org Units
+            CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0 END +
+            CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
+            -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0 END +
+            -- Static User Checks
+            CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
+            CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0 END +
+            CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
+            CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
+            CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
+            CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
+            CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return the entire matchpoint
+    RETURN matchpoint;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+  RETURNS integer AS
+$func$
+DECLARE
+    requestor_object    actor.usr%ROWTYPE;
+    user_object         actor.usr%ROWTYPE;
+    item_object         asset.copy%ROWTYPE;
+    item_cn_object      asset.call_number%ROWTYPE;
+    rec_descriptor      metabib.rec_descriptor%ROWTYPE;
+    matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
+    weights             config.hold_matrix_weights%ROWTYPE;
+    denominator         INT;
+BEGIN
+    SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
+    SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
+
+    -- The item's owner should probably be the one determining if the item is holdable
+    -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+    -- This flag will allow for setting it to the owning library (where the call number "lives")
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+    -- Grab the closest set circ weight setting.
+    IF NOT FOUND THEN
+        -- Default to circ library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    ELSE
+        -- Flag is set, use owning library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    END IF;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.user_home_ou    := 5;
+        weights.request_ou      := 5;
+        weights.pickup_ou       := 5;
+        weights.item_owning_ou  := 5;
+        weights.item_circ_ou    := 5;
+        weights.usr_grp         := 7;
+        weights.requestor_grp   := 8;
+        weights.circ_modifier   := 4;
+        weights.marc_type       := 3;
+        weights.marc_form       := 2;
+        weights.marc_vr_format  := 1;
+        weights.juvenile_flag   := 4;
+        weights.ref_flag        := 0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+            SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+    -- This may be better implemented as part of the upgrade script?
+    -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+    -- Then remove this flag, of course.
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
+
+    IF FOUND THEN
+        -- Note: This, to me, is REALLY hacky. I put it in anyway.
+        -- If you can't tell, this is a single call swap on two variables.
+        SELECT INTO user_object.profile, requestor_object.profile
+                    requestor_object.profile, user_object.profile;
+    END IF;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.hold_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+            LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+            LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+            AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
+            -- Org Units
+            AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+            -- Static User Checks
+            AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
+            -- Static Item Checks
+            AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0 END +
+            CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0 END +
+            -- Org Units
+            CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0 END +
+            CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0 END +
+            CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
+            -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
+            CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
+            CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
+            CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return just the ID for now
+    RETURN matchpoint.id;
+END;
+$func$ LANGUAGE 'plpgsql';
+
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+
+INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
+    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+
+INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
+    (true, 1, 1, 1);
+
+COMMIT;
index d1f1829..06b59a8 100644 (file)
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_recurring_fine "Circulation Recurring Fine Rules">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_max_fine "Circulation Max Fine Rules">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_age_hold_protect "Age Hold Protect Rules">
+<!ENTITY staff.main.menu.admin.server_admin.conify.config_circ_weights "Circulation Matchpoint Weights">
+<!ENTITY staff.main.menu.admin.server_admin.conify.config_hold_weights "Hold Matchpoint Weights">
+<!ENTITY staff.main.menu.admin.server_admin.conify.config_weight_assoc "Weights Association">
 <!ENTITY staff.main.menu.admin.server_admin.conify.global_flag.label "Global Flags">
 
 <!ENTITY staff.main.menu.admin.server_admin.acq.label "Acquisitions">
diff --git a/Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2 b/Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2
new file mode 100644 (file)
index 0000000..0615abf
--- /dev/null
@@ -0,0 +1,28 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Circ Matrix Weights' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+        <div>Circ Matrix Weights</div>
+        <div>
+            <button dojoType='dijit.form.Button' onClick='ruleCircWeightsGrid.showCreateDialog()'>New Weight Set</button>
+            <button dojoType='dijit.form.Button' onClick='ruleCircWeightsGrid.deleteSelected()'>Delete Selected</button>
+        </div>
+    </div>
+    <div>
+    <table  jsId="ruleCircWeightsGrid"
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name']"
+            suppressFields="['id']"
+            query="{id: '*'}"
+            fmClass='ccmw'
+            editOnEnter='true'/>
+</div>
+
+<script type="text/javascript">
+    dojo.require('openils.Util');
+    dojo.require('openils.widget.AutoGrid');
+    openils.Util.addOnLoad( function() { ruleCircWeightsGrid.loadAll(); } );
+</script>
+[% END %]
+
+
index 3c8bbbb..55ca6b5 100644 (file)
@@ -9,7 +9,6 @@
             autoHeight='true'
             dojoType="openils.widget.AutoGrid" 
             fieldOrder="['id', 'strict_ou_match', 'user_home_ou', 'request_ou', 'pickup_ou', 'item_owning_ou', 'item_circ_ou', 'requestor_grp', 'circ_modifier']"
-            suppressFields="['usr_grp']"
             defaultCellWidth='"auto"'
             query="{id: '*'}" 
             fmClass='chmm' 
diff --git a/Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2 b/Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2
new file mode 100644 (file)
index 0000000..1aa25eb
--- /dev/null
@@ -0,0 +1,28 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Hold Matrix Weights' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+        <div>Hold Matrix Weights</div>
+        <div>
+            <button dojoType='dijit.form.Button' onClick='ruleHoldWeightsGrid.showCreateDialog()'>New Weight Set</button>
+            <button dojoType='dijit.form.Button' onClick='ruleHoldWeightsGrid.deleteSelected()'>Delete Selected</button>
+        </div>
+    </div>
+    <div>
+    <table  jsId="ruleHoldWeightsGrid"
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name']"
+            suppressFields="['id']"
+            query="{id: '*'}"
+            fmClass='chmw'
+            editOnEnter='true'/>
+</div>
+
+<script type="text/javascript">
+    dojo.require('openils.Util');
+    dojo.require('openils.widget.AutoGrid');
+    openils.Util.addOnLoad( function() { ruleHoldWeightsGrid.loadAll(); } );
+</script>
+[% END %]
+
+
diff --git a/Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2 b/Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2
new file mode 100644 (file)
index 0000000..8f66e81
--- /dev/null
@@ -0,0 +1,28 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Matrix Weight Associations' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+        <div>Matrix Weight Associations</div>
+        <div>
+            <button dojoType='dijit.form.Button' onClick='ruleWeightAssocGrid.showCreateDialog()'>New Weight Association</button>
+            <button dojoType='dijit.form.Button' onClick='ruleWeightAssocGrid.deleteSelected()'>Delete Selected</button>
+        </div>
+    </div>
+    <div>
+    <table  jsId="ruleWeightAssocGrid"
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['active','org_unit','circ_weights','hold_weights']"
+            suppressFields="['id']"
+            query="{id: '*'}"
+            fmClass='cwa'
+            editOnEnter='true'/>
+</div>
+
+<script type="text/javascript">
+    dojo.require('openils.Util');
+    dojo.require('openils.widget.AutoGrid');
+    openils.Util.addOnLoad( function() { ruleWeightAssocGrid.loadAll(); } );
+</script>
+[% END %]
+
+
index 077955a..3d7eda8 100644 (file)
@@ -629,6 +629,18 @@ main.menu.prototype = {
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/config/rule_age_hold_protect'); }
             ],
+            'cmd_server_admin_config_circ_weights' : [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/config/circ_matrix_weights'); }
+            ],
+            'cmd_server_admin_config_hold_weights' : [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/config/hold_matrix_weights'); }
+            ],
+            'cmd_server_admin_config_weight_assoc' : [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/config/weight_assoc'); }
+            ],
             'cmd_local_admin_external_text_editor' : [
                 ['oncommand'],
                 function() {
index 197e660..a017100 100644 (file)
     <command id="cmd_server_admin_booking_resource_attr" />
     <command id="cmd_server_admin_booking_resource_attr_value" />
     <command id="cmd_server_admin_booking_resource_attr_map" />
+    <command id="cmd_server_admin_config_circ_weights" />
+    <command id="cmd_server_admin_config_hold_weights" />
+    <command id="cmd_server_admin_config_weight_assoc" />
 </commandset>
 
 
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_rule_recurring_fine;" command="cmd_server_admin_config_rule_recurring_fine"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_rule_max_fine;" command="cmd_server_admin_config_rule_max_fine"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_rule_age_hold_protect;" command="cmd_server_admin_config_rule_age_hold_protect"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.config_circ_weights;" command="cmd_server_admin_config_circ_weights"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.config_hold_weights;" command="cmd_server_admin_config_hold_weights"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.config_weight_assoc;" command="cmd_server_admin_config_weight_assoc"/>
                 <menu id="main.menu.admin.server.acq" label="&staff.main.menu.admin.server_admin.acq.label;" accesskey="&staff.main.menu.admin.server_admin.acq.accesskey;">
                     <menupopup id="main.menu.admin.server.acq.popup">
                         <menuitem label="&staff.main.menu.admin.server_admin.acq.fund.label;" accesskey="&staff.main.menu.admin.server_admin.acq.fund.accesskey;" command="cmd_server_admin_acq_fund" />