Advertisement
roganhamby

Testing - Holds Policy Script

Jan 26th, 2015
251
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*  scratch
  2.  
  3. action.find_hold_matrix_matchpoint
  4. pickup_ou int
  5. request_ou int
  6. match_item bigint
  7. match_user int
  8. match_requestor int
  9.  
  10. rogan.holds_testing
  11. id
  12. pickup_ou, request_ou -> should these go recursively through org_units at branch level or be spelled out?
  13. match_item -> can be selected by a query, how many at different locations?
  14. match_user -> same as item
  15. match_requestor -> should test staff as well as user
  16. expected result
  17.  
  18. */
  19.  
  20.  
  21. CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
  22.   RETURNS integer AS
  23. $func$
  24. DECLARE
  25.     requestor_object    actor.usr%ROWTYPE;
  26.     user_object         actor.usr%ROWTYPE;
  27.     item_object         asset.copy%ROWTYPE;
  28.     item_cn_object      asset.call_number%ROWTYPE;
  29.     my_item_age         INTERVAL;
  30.     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
  31.     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
  32.     weights             config.hold_matrix_weights%ROWTYPE;
  33.     denominator         NUMERIC(6,2);
  34.     v_pickup_ou         ALIAS FOR pickup_ou;
  35.     v_request_ou         ALIAS FOR request_ou;
  36. BEGIN
  37.     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
  38.     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
  39.     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
  40.     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
  41.     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
  42.  
  43.     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
  44.  
  45.     -- The item's owner should probably be the one determining if the item is holdable
  46.     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
  47.     -- This flag will allow for setting it to the owning library (where the call number "lives")
  48.     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
  49.  
  50.     -- Grab the closest set circ weight setting.
  51.     IF NOT FOUND THEN
  52.         -- Default to circ library
  53.         SELECT INTO weights hw.*
  54.           FROM config.weight_assoc wa
  55.                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
  56.                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
  57.           WHERE active
  58.           ORDER BY d.distance
  59.           LIMIT 1;
  60.     ELSE
  61.         -- Flag is set, use owning library
  62.         SELECT INTO weights hw.*
  63.           FROM config.weight_assoc wa
  64.                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
  65.                JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
  66.           WHERE active
  67.           ORDER BY d.distance
  68.           LIMIT 1;
  69.     END IF;
  70.  
  71.     -- No weights? Bad admin! Defaults to handle that anyway.
  72.     IF weights.id IS NULL THEN
  73.         weights.user_home_ou    := 5.0;
  74.         weights.request_ou      := 5.0;
  75.         weights.pickup_ou       := 5.0;
  76.         weights.item_owning_ou  := 5.0;
  77.         weights.item_circ_ou    := 5.0;
  78.         weights.usr_grp         := 7.0;
  79.         weights.requestor_grp   := 8.0;
  80.         weights.circ_modifier   := 4.0;
  81.         weights.marc_type       := 3.0;
  82.         weights.marc_form       := 2.0;
  83.         weights.marc_bib_level  := 1.0;
  84.         weights.marc_vr_format  := 1.0;
  85.         weights.juvenile_flag   := 4.0;
  86.         weights.ref_flag        := 0.0;
  87.         weights.item_age        := 0.0;
  88.     END IF;
  89.  
  90.     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
  91.     -- If you break your org tree with funky parenting this may be wrong
  92.     -- 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
  93.     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
  94.     WITH all_distance(distance) AS (
  95.             SELECT depth AS distance FROM actor.org_unit_type
  96.         UNION
  97.             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
  98.     )
  99.     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
  100.  
  101.     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
  102.     -- This may be better implemented as part of the upgrade script?
  103.     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
  104.     -- Then remove this flag, of course.
  105.     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
  106.  
  107.     IF FOUND THEN
  108.         -- Note: This, to me, is REALLY hacky. I put it in anyway.
  109.         -- If you can't tell, this is a single call swap on two variables.
  110.         SELECT INTO user_object.profile, requestor_object.profile
  111.                     requestor_object.profile, user_object.profile;
  112.     END IF;
  113.  
  114.     -- Select the winning matchpoint into the matchpoint variable for returning
  115.     SELECT INTO matchpoint m.*
  116.       FROM  config.hold_matrix_matchpoint m
  117.             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
  118.             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
  119.             LEFT JOIN actor.org_unit_ancestors_distance( v_pickup_ou ) puoua ON m.pickup_ou = puoua.id
  120.             LEFT JOIN actor.org_unit_ancestors_distance( v_request_ou ) rqoua ON m.request_ou = rqoua.id
  121.             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
  122.             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
  123.             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
  124.       WHERE m.active
  125.             -- Permission Groups
  126.          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
  127.             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
  128.             -- Org Units
  129.             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
  130.             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
  131.             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
  132.             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
  133.             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
  134.             -- Static User Checks
  135.             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
  136.             -- Static Item Checks
  137.             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
  138.             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
  139.             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
  140.             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
  141.             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
  142.             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
  143.             AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
  144.       ORDER BY
  145.             -- Permission Groups
  146.             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
  147.             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
  148.             -- Org Units
  149.             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
  150.             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
  151.             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
  152.             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
  153.             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
  154.             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
  155.             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
  156.             -- Static Item Checks
  157.             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
  158.             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
  159.             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
  160.             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
  161.             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
  162.             -- Item age has a slight adjustment to weight based on value.
  163.             -- This should ensure that a shorter age limit comes first when all else is equal.
  164.             -- NOTE: This assumes that intervals will normally be in days.
  165.             CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
  166.             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
  167.             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
  168.             m.id;
  169.  
  170.     -- Return just the ID for now
  171.     RETURN matchpoint.id;
  172. END;
  173. $func$ LANGUAGE 'plpgsql';
  174.  
  175.  
  176.  
  177. CREATE TYPE action.matrix_test_result AS ( success BOOL, matchpoint INT, fail_part TEXT );
  178. 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$
  179. DECLARE
  180.     matchpoint_id        INT;
  181.     user_object        actor.usr%ROWTYPE;
  182.     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
  183.     standing_penalty    config.standing_penalty%ROWTYPE;
  184.     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
  185.     transit_source        actor.org_unit%ROWTYPE;
  186.     item_object        asset.copy%ROWTYPE;
  187.     item_cn_object     asset.call_number%ROWTYPE;
  188.     item_status_object  config.copy_status%ROWTYPE;
  189.     item_location_object    asset.copy_location%ROWTYPE;
  190.     ou_skip              actor.org_unit_setting%ROWTYPE;
  191.     result            action.matrix_test_result;
  192.     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
  193.     use_active_date   TEXT;
  194.     age_protect_date  TIMESTAMP WITH TIME ZONE;
  195.     hold_count        INT;
  196.     hold_transit_prox    INT;
  197.     frozen_hold_count    INT;
  198.     context_org_list    INT[];
  199.     done            BOOL := FALSE;
  200.     hold_penalty TEXT;
  201.     v_pickup_ou ALIAS FOR pickup_ou;
  202.     v_request_ou ALIAS FOR request_ou;
  203. BEGIN
  204.     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
  205.     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
  206.  
  207.     result.success := TRUE;
  208.  
  209.     -- The HOLD penalty block only applies to new holds.
  210.     -- The CAPTURE penalty block applies to existing holds.
  211.     hold_penalty := 'HOLD';
  212.     IF retargetting THEN
  213.         hold_penalty := 'CAPTURE';
  214.     END IF;
  215.  
  216.     -- Fail if we couldn't find a user
  217.     IF user_object.id IS NULL THEN
  218.         result.fail_part := 'no_user';
  219.         result.success := FALSE;
  220.         done := TRUE;
  221.         RETURN NEXT result;
  222.         RETURN;
  223.     END IF;
  224.  
  225.     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
  226.  
  227.     -- Fail if we couldn't find a copy
  228.     IF item_object.id IS NULL THEN
  229.         result.fail_part := 'no_item';
  230.         result.success := FALSE;
  231.         done := TRUE;
  232.         RETURN NEXT result;
  233.         RETURN;
  234.     END IF;
  235.  
  236.     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
  237.     result.matchpoint := matchpoint_id;
  238.  
  239.     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
  240.  
  241.     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
  242.     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
  243.         result.fail_part := 'circ.holds.target_skip_me';
  244.         result.success := FALSE;
  245.         done := TRUE;
  246.         RETURN NEXT result;
  247.         RETURN;
  248.     END IF;
  249.  
  250.     -- Fail if user is barred
  251.     IF user_object.barred IS TRUE THEN
  252.         result.fail_part := 'actor.usr.barred';
  253.         result.success := FALSE;
  254.         done := TRUE;
  255.         RETURN NEXT result;
  256.         RETURN;
  257.     END IF;
  258.  
  259.     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
  260.     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
  261.     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
  262.  
  263.     -- Fail if we couldn't find any matchpoint (requires a default)
  264.     IF matchpoint_id IS NULL THEN
  265.         result.fail_part := 'no_matchpoint';
  266.         result.success := FALSE;
  267.         done := TRUE;
  268.         RETURN NEXT result;
  269.         RETURN;
  270.     END IF;
  271.  
  272.     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
  273.  
  274.     IF hold_test.holdable IS FALSE THEN
  275.         result.fail_part := 'config.hold_matrix_test.holdable';
  276.         result.success := FALSE;
  277.         done := TRUE;
  278.         RETURN NEXT result;
  279.     END IF;
  280.  
  281.     IF item_object.holdable IS FALSE THEN
  282.         result.fail_part := 'item.holdable';
  283.         result.success := FALSE;
  284.         done := TRUE;
  285.         RETURN NEXT result;
  286.     END IF;
  287.  
  288.     IF item_status_object.holdable IS FALSE THEN
  289.         result.fail_part := 'status.holdable';
  290.         result.success := FALSE;
  291.         done := TRUE;
  292.         RETURN NEXT result;
  293.     END IF;
  294.  
  295.     IF item_location_object.holdable IS FALSE THEN
  296.         result.fail_part := 'location.holdable';
  297.         result.success := FALSE;
  298.         done := TRUE;
  299.         RETURN NEXT result;
  300.     END IF;
  301.  
  302.     IF hold_test.transit_range IS NOT NULL THEN
  303.         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
  304.         IF hold_test.distance_is_from_owner THEN
  305.             SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
  306.         ELSE
  307.             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
  308.         END IF;
  309.  
  310.         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
  311.  
  312.         IF NOT FOUND THEN
  313.             result.fail_part := 'transit_range';
  314.             result.success := FALSE;
  315.             done := TRUE;
  316.             RETURN NEXT result;
  317.         END IF;
  318.     END IF;
  319.  
  320.     FOR standing_penalty IN
  321.         SELECT  DISTINCT csp.*
  322.           FROM  actor.usr_standing_penalty usp
  323.                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
  324.           WHERE usr = match_user
  325.                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
  326.                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
  327.                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
  328.  
  329.         result.fail_part := standing_penalty.name;
  330.         result.success := FALSE;
  331.         done := TRUE;
  332.         RETURN NEXT result;
  333.     END LOOP;
  334.  
  335.     IF hold_test.stop_blocked_user IS TRUE THEN
  336.         FOR standing_penalty IN
  337.             SELECT  DISTINCT csp.*
  338.               FROM  actor.usr_standing_penalty usp
  339.                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
  340.               WHERE usr = match_user
  341.                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
  342.                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
  343.                     AND csp.block_list LIKE '%CIRC%' LOOP
  344.    
  345.             result.fail_part := standing_penalty.name;
  346.             result.success := FALSE;
  347.             done := TRUE;
  348.             RETURN NEXT result;
  349.         END LOOP;
  350.     END IF;
  351.  
  352.     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
  353.         SELECT    INTO hold_count COUNT(*)
  354.           FROM    action.hold_request
  355.           WHERE    usr = match_user
  356.             AND fulfillment_time IS NULL
  357.             AND cancel_time IS NULL
  358.             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
  359.  
  360.         IF hold_count >= hold_test.max_holds THEN
  361.             result.fail_part := 'config.hold_matrix_test.max_holds';
  362.             result.success := FALSE;
  363.             done := TRUE;
  364.             RETURN NEXT result;
  365.         END IF;
  366.     END IF;
  367.  
  368.     IF item_object.age_protect IS NOT NULL THEN
  369.         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
  370.         IF hold_test.distance_is_from_owner THEN
  371.             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
  372.         ELSE
  373.             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
  374.         END IF;
  375.         IF use_active_date = 'true' THEN
  376.             age_protect_date := COALESCE(item_object.active_date, NOW());
  377.         ELSE
  378.             age_protect_date := item_object.create_date;
  379.         END IF;
  380.         IF age_protect_date + age_protect_object.age > NOW() THEN
  381.             IF hold_test.distance_is_from_owner THEN
  382.                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
  383.                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
  384.             ELSE
  385.                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
  386.             END IF;
  387.  
  388.             IF hold_transit_prox > age_protect_object.prox THEN
  389.                 result.fail_part := 'config.rule_age_hold_protect.prox';
  390.                 result.success := FALSE;
  391.                 done := TRUE;
  392.                 RETURN NEXT result;
  393.             END IF;
  394.         END IF;
  395.     END IF;
  396.  
  397.     IF NOT done THEN
  398.         RETURN NEXT result;
  399.     END IF;
  400.  
  401.     RETURN;
  402. END;
  403. $func$ LANGUAGE plpgsql;
  404.  
  405. CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT ) RETURNS SETOF action.matrix_test_result AS $func$
  406.     SELECT * FROM action.hold_request_permit_test( $1, $2, $3, $4, $5, FALSE );
  407. $func$ LANGUAGE SQL;
  408.  
  409. CREATE OR REPLACE FUNCTION action.hold_retarget_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT ) RETURNS SETOF action.matrix_test_result AS $func$
  410.     SELECT * FROM action.hold_request_permit_test( $1, $2, $3, $4, $5, TRUE );
  411. $func$ LANGUAGE SQL;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement