From: Thomas Berezansky Date: Tue, 7 Feb 2012 22:26:03 +0000 (-0500) Subject: New Circ Limits X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=5e2bad24b7743d5129999d0dfb6cf4352e2ef91d;p=evergreen%2Fmasslnc.git New Circ Limits Replace the old "Circ Mod Test" limit system with a more flexible system. In addition to circ modifiers this system supports "Limit Groups" that are automatically applied (by default) to any circulation checking them. This can be overidden by setting the "Check Only" flag when linking a Limit Group to a Limit Set. Both the limit groups and circ modifiers are linked to "Limit Sets" that act similarly to rules. Each Set can be attached to 0 or more circulation matchpoints. Each Limit set supports a number of items out (0 replaces infinite), depth in the org tree to start counting at (0 for up to the top, 1 for 1 below, etc), and a global flag (to check everywhere below the depth point, rather than just those circulations that happend at ancestors/descendants). When a Limit Set is linked to a Circulation Matchpoint it can be made inactive and has a fallthrough flag. When the fallthrough flag is enabled the Limit Set will be used whenever the matchpoint is involved with making a decision. When it is disabled the Limit Set will only be used when the matchpoint is the most specific matchpoint used in making the decision. Limit Groups management can be found on the server administration menu. Limit Sets management can be found on the local administration menu. Limit Set -> Matchpoint linking is done via editing Circulation Policies. The upgrade script does not remove the old tables in case something goes wrong with migrating the information contained within them. Signed-off-by: Thomas Berezansky Conflicts: Open-ILS/web/opac/locale/en-US/lang.dtd Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul Signed-off-by: Jason Stephenson Signed-off-by: Mike Rylander --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 1894f7759c..f53638e122 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1470,15 +1470,58 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1493,33 +1536,60 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + - - - - - - - - - - + + + + + + + + + + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm index e5d572c989..731131dec3 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm @@ -539,6 +539,7 @@ my @AUTOLOAD_FIELDS = qw/ retarget_mode hold_as_transit fake_hold_dest + limit_groups /; @@ -1188,6 +1189,8 @@ sub run_indb_circ_test { } $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule})); $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date})); + # Grab the *last* response for limit_groups, where it is more likely to be filled + $self->limit_groups($results->[-1]->{limit_groups}); } return $self->matrix_test_result($results); @@ -1487,6 +1490,10 @@ sub do_checkout { # refresh the circ to force local time zone for now $self->circ($self->editor->retrieve_action_circulation($self->circ->id)); + if($self->limit_groups) { + $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] }); + } + $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT); $self->update_copy; return if $self->bail_out; diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql index 6d82e7383a..7b9e2eb5f2 100644 --- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql +++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql @@ -86,20 +86,63 @@ CREATE TABLE config.circ_matrix_matchpoint ( -- Nulls don't count for a constraint match, so we have to coalesce them into something that does. CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active; --- Tests for max items out by circ_modifier -CREATE TABLE config.circ_matrix_circ_mod_test ( - id SERIAL PRIMARY KEY, +-- Limit groups for circ counting +CREATE TABLE config.circ_limit_group ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + description TEXT +); + +-- Limit sets +CREATE TABLE config.circ_limit_set ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + owning_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, + items_out INT NOT NULL, -- Total current active circulations must be less than this. 0 means skip counting (always pass) + depth INT NOT NULL DEFAULT 0, -- Depth count starts at + global BOOL NOT NULL DEFAULT FALSE, -- If enabled, include everything below depth, otherwise ancestors/descendants only + description TEXT +); + +-- Linkage between matchpoints and limit sets +CREATE TABLE config.circ_matrix_limit_set_map ( + id SERIAL PRIMARY KEY, matchpoint INT NOT NULL REFERENCES config.circ_matrix_matchpoint (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, - items_out INT NOT NULL -- Total current active circulations must be less than this, NULL means skip (always pass) + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + fallthrough BOOL NOT NULL DEFAULT FALSE, -- If true fallthrough will grab this rule as it goes along + active BOOL NOT NULL DEFAULT TRUE, + CONSTRAINT circ_limit_set_once_per_matchpoint UNIQUE (matchpoint, limit_set) +); + +-- Linkage between limit sets and circ mods +CREATE TABLE config.circ_limit_set_circ_mod_map ( + id SERIAL PRIMARY KEY, + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + circ_mod TEXT NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT cm_once_per_set UNIQUE (limit_set, circ_mod) +); + +-- Linkage between limit sets and limit groups +CREATE TABLE config.circ_limit_set_group_map ( + id SERIAL PRIMARY KEY, + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + limit_group INT NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + check_only BOOL NOT NULL DEFAULT FALSE, -- If true, don't accumulate this limit_group for storing with the circulation + CONSTRAINT clg_once_per_set UNIQUE (limit_set, limit_group) ); -CREATE TABLE config.circ_matrix_circ_mod_test_map ( - id SERIAL PRIMARY KEY, - circ_mod_test INT NOT NULL REFERENCES config.circ_matrix_circ_mod_test (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, - circ_mod TEXT NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, - CONSTRAINT cm_once_per_test UNIQUE (circ_mod_test, circ_mod) +-- Linkage between limit groups and circulations +CREATE TABLE action.circulation_limit_group_map ( + circ BIGINT NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + limit_group INT NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + PRIMARY KEY (circ, limit_group) ); +-- Function for populating the circ/limit group mappings +CREATE OR REPLACE FUNCTION action.link_circ_limit_groups ( BIGINT, INT[] ) RETURNS VOID AS $func$ + INSERT INTO action.circulation_limit_group_map(circ, limit_group) SELECT $1, id FROM config.circ_limit_group WHERE id IN (SELECT * FROM UNNEST($2)); +$func$ LANGUAGE SQL; + CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] ); CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$ @@ -355,7 +398,7 @@ BEGIN END; $func$ LANGUAGE PLPGSQL; -CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL ); +CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL, limit_groups INT[] ); CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$ DECLARE user_object actor.usr%ROWTYPE; @@ -366,8 +409,7 @@ DECLARE result action.circ_matrix_test_result; circ_test action.found_circ_matrix_matchpoint; circ_matchpoint config.circ_matrix_matchpoint%ROWTYPE; - out_by_circ_mod config.circ_matrix_circ_mod_test%ROWTYPE; - circ_mod_map config.circ_matrix_circ_mod_test_map%ROWTYPE; + circ_limit_set config.circ_limit_set%ROWTYPE; hold_ratio action.hold_stats%ROWTYPE; penalty_type TEXT; items_out INT; @@ -466,7 +508,7 @@ BEGIN END IF; -- Use Circ OU for penalties and such - SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_ou ); + SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou ); IF renewal THEN penalty_type = '%RENEW%'; @@ -521,25 +563,50 @@ BEGIN END IF; END IF; - -- Fail if the user has too many items with specific circ_modifiers checked out - IF NOT renewal THEN - FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP - SELECT INTO items_out COUNT(*) - FROM action.circulation circ - JOIN asset.copy cp ON (cp.id = circ.target_copy) - WHERE circ.usr = match_user - AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) ) - AND circ.checkin_time IS NULL - AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL) - AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id); - IF items_out >= out_by_circ_mod.items_out THEN - result.fail_part := 'config.circ_matrix_circ_mod_test'; - result.success := FALSE; - done := TRUE; - RETURN NEXT result; + -- Fail if the user has too many items out by defined limit sets + FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls + JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id + WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR + ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough ) + ) LOOP + IF circ_limit_set.items_out > 0 AND NOT renewal THEN + SELECT INTO context_org_list ARRAY_AGG(aou.id) + FROM actor.org_unit_full_path( circ_ou ) aou + JOIN actor.org_unit_type aout ON aou.ou_type = aout.id + WHERE aout.depth >= circ_limit_set.depth; + IF circ_limit_set.global THEN + WITH RECURSIVE descendant_depth AS ( + SELECT ou.id, + ou.parent_ou + FROM actor.org_unit ou + WHERE ou.id IN (SELECT * FROM unnest(context_org_list)) + UNION + SELECT ou.id, + ou.parent_ou + FROM actor.org_unit ou + JOIN descendant_depth ot ON (ot.id = ou.parent_ou) + ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id); + END IF; + SELECT INTO items_out COUNT(DISTINCT circ.id) + FROM action.circulation circ + JOIN asset.copy copy ON (copy.id = circ.target_copy) + LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ) + WHERE circ.usr = match_user + AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list)) + AND circ.checkin_time IS NULL + AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL) + AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id) + OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id) + ); + IF items_out >= circ_limit_set.items_out THEN + result.fail_part := 'config.circ_matrix_circ_mod_test'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; END IF; - END LOOP; - END IF; + SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only; + END LOOP; -- If we passed everything, return the successful matchpoint IF NOT done THEN diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql new file mode 100644 index 0000000000..59b38f6d0d --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.circ_limits.sql @@ -0,0 +1,317 @@ +-- Limit groups for circ counting +CREATE TABLE config.circ_limit_group ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + description TEXT +); + +-- Limit sets +CREATE TABLE config.circ_limit_set ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + owning_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, + items_out INT NOT NULL, -- Total current active circulations must be less than this. 0 means skip counting (always pass) + depth INT NOT NULL DEFAULT 0, -- Depth count starts at + global BOOL NOT NULL DEFAULT FALSE, -- If enabled, include everything below depth, otherwise ancestors/descendants only + description TEXT +); + +-- Linkage between matchpoints and limit sets +CREATE TABLE config.circ_matrix_limit_set_map ( + id SERIAL PRIMARY KEY, + matchpoint INT NOT NULL REFERENCES config.circ_matrix_matchpoint (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + fallthrough BOOL NOT NULL DEFAULT FALSE, -- If true fallthrough will grab this rule as it goes along + active BOOL NOT NULL DEFAULT TRUE, + CONSTRAINT circ_limit_set_once_per_matchpoint UNIQUE (matchpoint, limit_set) +); + +-- Linkage between limit sets and circ mods +CREATE TABLE config.circ_limit_set_circ_mod_map ( + id SERIAL PRIMARY KEY, + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + circ_mod TEXT NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT cm_once_per_set UNIQUE (limit_set, circ_mod) +); + +-- Linkage between limit sets and limit groups +CREATE TABLE config.circ_limit_set_group_map ( + id SERIAL PRIMARY KEY, + limit_set INT NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + limit_group INT NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + check_only BOOL NOT NULL DEFAULT FALSE, -- If true, don't accumulate this limit_group for storing with the circulation + CONSTRAINT clg_once_per_set UNIQUE (limit_set, limit_group) +); + +-- Linkage between limit groups and circulations +CREATE TABLE action.circulation_limit_group_map ( + circ BIGINT NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + limit_group INT NOT NULL REFERENCES config.circ_limit_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + PRIMARY KEY (circ, limit_group) +); + +-- Function for populating the circ/limit group mappings +CREATE OR REPLACE FUNCTION action.link_circ_limit_groups ( BIGINT, INT[] ) RETURNS VOID AS $func$ + INSERT INTO action.circulation_limit_group_map(circ, limit_group) SELECT $1, id FROM config.circ_limit_group WHERE id IN (SELECT * FROM UNNEST($2)); +$func$ LANGUAGE SQL; + +DROP TYPE IF EXISTS action.circ_matrix_test_result CASCADE; +CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL, limit_groups INT[] ); + +CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$ +DECLARE + user_object actor.usr%ROWTYPE; + standing_penalty config.standing_penalty%ROWTYPE; + item_object asset.copy%ROWTYPE; + item_status_object config.copy_status%ROWTYPE; + item_location_object asset.copy_location%ROWTYPE; + result action.circ_matrix_test_result; + circ_test action.found_circ_matrix_matchpoint; + circ_matchpoint config.circ_matrix_matchpoint%ROWTYPE; + circ_limit_set config.circ_limit_set%ROWTYPE; + hold_ratio action.hold_stats%ROWTYPE; + penalty_type TEXT; + items_out INT; + context_org_list INT[]; + done BOOL := FALSE; +BEGIN + -- Assume success unless we hit a failure condition + result.success := TRUE; + + -- Need user info to look up matchpoints + SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted; + + -- (Insta)Fail if we couldn't find the user + IF user_object.id IS NULL THEN + result.fail_part := 'no_user'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + RETURN; + END IF; + + -- Need item info to look up matchpoints + SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted; + + -- (Insta)Fail if we couldn't find the item + IF item_object.id IS NULL THEN + result.fail_part := 'no_item'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + RETURN; + END IF; + + SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal); + + circ_matchpoint := circ_test.matchpoint; + result.matchpoint := circ_matchpoint.id; + result.circulate := circ_matchpoint.circulate; + result.duration_rule := circ_matchpoint.duration_rule; + result.recurring_fine_rule := circ_matchpoint.recurring_fine_rule; + result.max_fine_rule := circ_matchpoint.max_fine_rule; + result.hard_due_date := circ_matchpoint.hard_due_date; + result.renewals := circ_matchpoint.renewals; + result.grace_period := circ_matchpoint.grace_period; + result.buildrows := circ_test.buildrows; + + -- (Insta)Fail if we couldn't find a matchpoint + IF circ_test.success = false THEN + result.fail_part := 'no_matchpoint'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + RETURN; + END IF; + + -- All failures before this point are non-recoverable + -- Below this point are possibly overridable failures + + -- Fail if the user is barred + IF user_object.barred IS TRUE THEN + result.fail_part := 'actor.usr.barred'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + + -- Fail if the item can't circulate + IF item_object.circulate IS FALSE THEN + result.fail_part := 'asset.copy.circulate'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + + -- Fail if the item isn't in a circulateable status on a non-renewal + IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN + result.fail_part := 'asset.copy.status'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + -- Alternately, fail if the item isn't checked out on a renewal + ELSIF renewal AND item_object.status <> 1 THEN + result.fail_part := 'asset.copy.status'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + + -- Fail if the item can't circulate because of the shelving location + SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location; + IF item_location_object.circulate IS FALSE THEN + result.fail_part := 'asset.copy_location.circulate'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + + -- Use Circ OU for penalties and such + SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou ); + + IF renewal THEN + penalty_type = '%RENEW%'; + ELSE + penalty_type = '%CIRC%'; + END IF; + + FOR standing_penalty IN + SELECT DISTINCT csp.* + FROM actor.usr_standing_penalty usp + JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty) + WHERE usr = match_user + AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) ) + AND (usp.stop_date IS NULL or usp.stop_date > NOW()) + AND csp.block_list LIKE penalty_type LOOP + + result.fail_part := standing_penalty.name; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END LOOP; + + -- Fail if the test is set to hard non-circulating + IF circ_matchpoint.circulate IS FALSE THEN + result.fail_part := 'config.circ_matrix_test.circulate'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + + -- Fail if the total copy-hold ratio is too low + IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN + SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item); + IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN + result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + END IF; + + -- Fail if the available copy-hold ratio is too low + IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN + IF hold_ratio.hold_count IS NULL THEN + SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item); + END IF; + IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN + result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + END IF; + + -- Fail if the user has too many items out by defined limit sets + FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls + JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id + WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR + ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough ) + ) LOOP + IF circ_limit_set.items_out > 0 AND NOT renewal THEN + SELECT INTO context_org_list ARRAY_AGG(aou.id) + FROM actor.org_unit_full_path( circ_ou ) aou + JOIN actor.org_unit_type aout ON aou.ou_type = aout.id + WHERE aout.depth >= circ_limit_set.depth; + IF circ_limit_set.global THEN + WITH RECURSIVE descendant_depth AS ( + SELECT ou.id, + ou.parent_ou + FROM actor.org_unit ou + WHERE ou.id IN (SELECT * FROM unnest(context_org_list)) + UNION + SELECT ou.id, + ou.parent_ou + FROM actor.org_unit ou + JOIN descendant_depth ot ON (ot.id = ou.parent_ou) + ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id); + END IF; + SELECT INTO items_out COUNT(DISTINCT circ.id) + FROM action.circulation circ + JOIN asset.copy copy ON (copy.id = circ.target_copy) + LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ) + WHERE circ.usr = match_user + AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list)) + AND circ.checkin_time IS NULL + AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL) + AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id) + OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id) + ); + IF items_out >= circ_limit_set.items_out THEN + result.fail_part := 'config.circ_matrix_circ_mod_test'; + result.success := FALSE; + done := TRUE; + RETURN NEXT result; + END IF; + END IF; + SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only; + END LOOP; + + -- If we passed everything, return the successful matchpoint + IF NOT done THEN + RETURN NEXT result; + END IF; + + RETURN; +END; +$func$ LANGUAGE plpgsql; + +-- We need to re-create these, as they got dropped with the type above. +CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$ + SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE ); +$func$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$ + SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE ); +$func$ LANGUAGE SQL; + +-- Temp function for migrating circ mod limits. +CREATE OR REPLACE FUNCTION evergreen.temp_migrate_circ_mod_limits() RETURNS VOID AS $func$ +DECLARE + circ_mod_group config.circ_matrix_circ_mod_test%ROWTYPE; + current_set INT; + circ_mod_count INT; +BEGIN + FOR circ_mod_group IN SELECT * FROM config.circ_matrix_circ_mod_test LOOP + INSERT INTO config.circ_limit_set(name, owning_lib, items_out, depth, global, description) + SELECT org_unit || ' : Matchpoint ' || circ_mod_group.matchpoint || ' : Circ Mod Test ' || circ_mod_group.id, org_unit, circ_mod_group.items_out, 0, false, 'Migrated from Circ Mod Test System' + FROM config.circ_matrix_matchpoint WHERE id = circ_mod_group.matchpoint + RETURNING id INTO current_set; + INSERT INTO config.circ_matrix_limit_set_map(matchpoint, limit_set, fallthrough, active) VALUES (circ_mod_group.matchpoint, current_set, false, true); + INSERT INTO config.circ_limit_set_circ_mod_map(limit_set, circ_mod) + SELECT current_set, circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = circ_mod_group.id; + SELECT INTO circ_mod_count count(id) FROM config.circ_limit_set_circ_mod_map WHERE limit_set = current_set; + RAISE NOTICE 'Created limit set with id % and % circ modifiers attached to matchpoint %', current_set, circ_mod_count, circ_mod_group.matchpoint; + END LOOP; +END; +$func$ LANGUAGE plpgsql; + +-- Run the temp function +SELECT * FROM evergreen.temp_migrate_circ_mod_limits(); + +-- Drop the temp function +DROP FUNCTION evergreen.temp_migrate_circ_mod_limits(); + +--Drop the old tables +--Not sure we want to do this. Keeping them may help "something went wrong" correction. +--DROP TABLE IF EXISTS config.circ_matrix_circ_mod_test_map, config.circ_matrix_circ_mod_test; diff --git a/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2 b/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2 new file mode 100644 index 0000000000..4f36e9ce2a --- /dev/null +++ b/Open-ILS/src/templates/conify/global/config/circ_limit_group.tt2 @@ -0,0 +1,36 @@ +[% WRAPPER base.tt2 %] +

Circulation Limit Group


+
+
Circulation Limit Group
+
+ + +
+
+ +
+ +
+
+ + + +[% END %] diff --git a/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2 b/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2 new file mode 100644 index 0000000000..213870610a --- /dev/null +++ b/Open-ILS/src/templates/conify/global/config/circ_limit_set.tt2 @@ -0,0 +1,78 @@ +[% ctx.page_title = 'Circulation Limit Set' %] +[% WRAPPER base.tt2 %] + +
+
Circulation Limit Set
+
+ + +
+
+
+ +
+
+ + + + +[% END %] + diff --git a/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2 b/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2 index b1d71501d2..67386f0ee9 100644 --- a/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2 +++ b/Open-ILS/src/templates/conify/global/config/circ_matrix_matchpoint.tt2 @@ -27,51 +27,37 @@ + - [% END %] diff --git a/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js b/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js new file mode 100644 index 0000000000..49d6aa8de7 --- /dev/null +++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_limit_set.js @@ -0,0 +1,250 @@ +dojo.require('dijit.layout.ContentPane'); +dojo.require('dijit.form.Button'); +dojo.require('openils.widget.AutoGrid'); +dojo.require('openils.widget.AutoFieldWidget'); +dojo.require('openils.PermaCrud'); +dojo.require('openils.widget.ProgressDialog'); + +var linkedEditor = null; +var circModEntryCache = []; +var limitGroupEntryCache = []; +var circModCache = {}; +var limitGroupCache = {}; +var curLinkedEditor; + +function load(){ + clsGrid.loadAll({order_by:{ccls:'name'}}); + clsGrid.onEditPane = buildEditPaneAdditions; + clsGrid.onPostUpdate = updateLinked; + clsGrid.onPostCreate = updateLinked; + linkedEditor = dojo.byId('linked-editor').parentNode.removeChild(dojo.byId('linked-editor')); + + // Cache circ mod/limit group info for later display + var pcrud = new openils.PermaCrud(); + var temp = pcrud.retrieveAll('ccm'); + dojo.forEach(temp, function(g) { circModCache[g.code()] = g; } ); + temp = pcrud.retrieveAll('cclg'); + dojo.forEach(temp, function(g) { limitGroupCache[g.id()] = g; } ); +} + +function byName(name, ctxt) { + return dojo.query('[name=' + name + ']', ctxt)[0]; +} + +function buildEditPaneAdditions(editPane) { + circModEntryCache = []; + limitGroupEntryCache = []; + var tr = document.createElement('tr'); + var td = document.createElement('td'); + td.setAttribute('colspan','2'); + // Explanation.... + // editPane.domNode.lastChild = Table + // .lastChild = Table Body + // .lastChild = Table Row containing Action Buttons + editPane.domNode.lastChild.lastChild.insertBefore(tr, editPane.domNode.lastChild.lastChild.lastChild); + tr.appendChild(td); + curLinkedEditor = linkedEditor.cloneNode(true); + td.appendChild(curLinkedEditor); + var circModTmpl = byName('circ-mod-entry-tbody', curLinkedEditor).removeChild(byName('circ-mod-entry-row', curLinkedEditor)); + var limitGroupTmpl = byName('limit-group-entry-tbody', curLinkedEditor).removeChild(byName('limit-group-entry-row', curLinkedEditor)); + + var cm_selector = new openils.widget.AutoFieldWidget({ + fmClass : 'cclscmm', + fmField : 'circ_mod', + parentNode : byName('circ-mod-selector', curLinkedEditor) + }); + cm_selector.build(); + + var lg_selector = new openils.widget.AutoFieldWidget({ + fmClass : 'cclsgm', + fmField : 'limit_group', + parentNode : byName('limit-group-selector', curLinkedEditor) + }); + lg_selector.build(); + + function addMod(code) { + var row = circModTmpl.cloneNode(true); + row.setAttribute('code', code); + byName('circ-mod', row).innerHTML = code + ' : ' + circModCache[code].name(); + byName('remove-circ-mod', row).onclick = function() { + byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).removeChild(row); + } + byName('circ-mod-entry-tbody', editPane.domNode).appendChild(row); + } + + function addGroup(group) { + var row = limitGroupTmpl.cloneNode(true); + row.setAttribute('limit_group', group); + byName('limit-group', row).innerHTML = limitGroupCache[group].name(); + byName('remove-limit-group', row).onclick = function() { + byName('limit-group-entry-tbody', clsGrid.editPane.domNode).removeChild(row); + } + byName('limit-group-entry-tbody', editPane.domNode).appendChild(row); + } + + byName('add-circ-mod', editPane.domNode).onclick = function() { + addMod(cm_selector.widget.attr('value')); + } + + byName('add-limit-group', editPane.domNode).onclick = function() { + addGroup(lg_selector.widget.attr('value')); + } + + // On edit we need to load existing entries. + // On create, not so much. + if(!editPane.fmObject) return; + var limitSet = editPane.fmObject.id(); + + if(editPane.mode == 'update') { + var pcrud = new openils.PermaCrud(); + circModEntryCache = pcrud.search('cclscmm', {limit_set: limitSet}); + limitGroupEntryCache = pcrud.search('cclsgm', {limit_set: limitSet}); + dojo.forEach(circModEntryCache, function(g) { addCircMod(circModTmpl, g); } ); + dojo.forEach(limitGroupEntryCache, function(g) { addLimitGroup(limitGroupTmpl, g); } ); + } +} + +function addCircMod(tmpl, circ_mod_entry) { + var row = tmpl.cloneNode(true); + var code = circ_mod_entry.circ_mod(); + row.setAttribute('code', code); + byName('circ-mod', row).innerHTML = code + ' : ' + circModCache[code].name(); + byName('remove-circ-mod', row).onclick = function() { + byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).removeChild(row); + } + byName('circ-mod-entry-tbody', clsGrid.editPane.domNode).appendChild(row); +} + +function addLimitGroup(tmpl, limit_group_entry) { + var row = tmpl.cloneNode(true); + var group = limit_group_entry.limit_group(); + row.setAttribute('limit_group', group); + byName('limit-group', row).innerHTML = limitGroupCache[group].name(); + if(limit_group_entry.check_only() == 't') { + byName('limit-group-check-only', row).setAttribute('checked', 'true'); + } + byName('remove-limit-group', row).onclick = function() { + byName('limit-group-entry-tbody', clsGrid.editPane.domNode).removeChild(row); + } + byName('limit-group-entry-tbody', clsGrid.editPane.domNode).appendChild(row); +} + +function updateLinked(fmObject, rowindex) { + var id = null; + if(rowindex != undefined && this.editPane && this.editPane.fmObject) { + // Edit, grab existing ID + id = this.editPane.fmObject.id(); + } else if(fmObject.id) { + // Create, grab new ID + id = fmObject.id(); + } + // If we don't have an ID, drop out. + if(id == null) return; + var pcrud = new openils.PermaCrud(); + progressDialog.show(true); + + var add = []; + var remove = []; + var update = []; + + // First up, circ mods. + var circ_mods = []; + dojo.query('[name=circ-mod-entry-row]', this.editPane.domNode).forEach( + function(row) { + var mod = row.getAttribute('code'); + circ_mods.push(mod); + if(!circModEntryCache.filter(function(i) { return (i.circ_mod() == mod); })[0]) { + var entry = new fieldmapper.cclscmm(); + entry.isnew(true); + entry.limit_set(id); + entry.circ_mod(mod); + add.push(entry); + } + } + ); + dojo.forEach(circModEntryCache, function(eMod) { + if(!circ_mods.filter(function(i) { return (i == eMod.circ_mod()); })[0]) { + eMod.isdeleted(true); + remove.push(eMod); + } + } + ); + + // Next, limit groups + var limit_groups = []; + dojo.query('[name=limit-group-entry-row]', this.editPane.domNode).forEach( + function(row) { + var group = row.getAttribute('limit_group'); + limit_groups.push(group); + var cached = limitGroupEntryCache.filter(function(i) { return (i.limit_group() == group); })[0]; + if(!cached) { + var entry = new fieldmapper.cclsgm(); + entry.isnew(true); + entry.limit_set(id); + entry.limit_group(group); + entry.check_only(byName('limit-group-check-only', row).checked ? 't' : 'f'); + add.push(entry); + } else { + var check_only = byName('limit-group-check-only', row).checked; + if(check_only != (cached.check_only() == 't')) { + cached.check_only(check_only ? 't' : 'f'); + cached.ischanged(true); + update.push(cached); + } + } + } + ); + dojo.forEach(limitGroupEntryCache, function(eGroup) { + if(!limit_groups.filter(function(i) { return (i == eGroup.limit_group()); })[0]) { + eGroup.isdeleted(true); + remove.push(eGroup); + } + } + ); + + function updateEntries() { + pcrud.update(update, { + oncomplete : function () { + progressDialog.hide(); + } + }); + } + + function removeEntries() { + pcrud.eliminate(remove, { + oncomplete : function () { + if(update.length) { + updateEntries(); + } else { + progressDialog.hide(); + } + } + }); + } + + function addEntries() { + pcrud.create(add, { + oncomplete : function () { + if(remove.length) { + removeEntries(); + } else if (update.length) { + updateEntries(); + } else { + progressDialog.hide(); + } + } + }); + } + + if(add.length) + addEntries(); + else if (remove.length) + removeEntries(); + else if (update.length) + updateEntries(); + else + progressDialog.hide(); +} + +openils.Util.addOnLoad(load); + diff --git a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js index 6e8dd508ea..175a15d0b3 100644 --- a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js +++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js @@ -5,11 +5,9 @@ dojo.require('openils.widget.AutoFieldWidget'); dojo.require('openils.PermaCrud'); dojo.require('openils.widget.ProgressDialog'); -var circModEditor = null; -var circModGroupTables = []; -var circModGroupCache = {}; -var circModEntryCache = {}; -var matchPoint; +var limitSetEditor = null; +var limitSetEntryCache = []; +var limitSetCache = {}; function load(){ cmGrid.overrideWidgetArgs.grp = {hrbefore : true}; @@ -27,7 +25,14 @@ function load(){ cmGrid.overrideWidgetArgs.hard_due_date = {inherits : true}; cmGrid.loadAll({order_by:{ccmm:'circ_modifier'}}); cmGrid.onEditPane = buildEditPaneAdditions; - circModEditor = dojo.byId('circ-mod-editor').parentNode.removeChild(dojo.byId('circ-mod-editor')); + cmGrid.onPostUpdate = updateLinked; + cmGrid.onPostCreate = updateLinked; + limitSetEditor = dojo.byId('limit-set-editor').parentNode.removeChild(dojo.byId('limit-set-editor')); + + // Cache limit set info for later display + var pcrud = new openils.PermaCrud(); + var temp = pcrud.retrieveAll('ccls'); + dojo.forEach(temp, function(g) { limitSetCache[g.id()] = g; } ); } function byName(name, ctxt) { @@ -35,174 +40,180 @@ function byName(name, ctxt) { } function buildEditPaneAdditions(editPane) { - if(!editPane.fmObject) return; - var node = circModEditor.cloneNode(true); - var tableTmpl = node.removeChild(byName('circ-mod-group-table', node)); - circModGroupTables = []; - matchPoint = editPane.fmObject.id(); + limitSetEntryCache = []; + var tr = document.createElement('tr'); + var td = document.createElement('td'); + td.setAttribute('colspan','2'); + // Explanation.... + // editPane.domNode.lastChild = Table + // .lastChild = Table Body + // .lastChild = Table Row containing Action Buttons + editPane.domNode.lastChild.lastChild.insertBefore(tr, editPane.domNode.lastChild.lastChild.lastChild); + tr.appendChild(td); + curLimitSetEditor = limitSetEditor.cloneNode(true); + td.appendChild(curLimitSetEditor); + var limitSetTmpl = byName('limit-set-entry-tbody', curLimitSetEditor).removeChild(byName('limit-set-entry-row', curLimitSetEditor)); + + var selector = new openils.widget.AutoFieldWidget({ + fmClass : 'ccmlsm', + fmField : 'limit_set', + parentNode : byName('limit-set-selector', curLimitSetEditor) + }); + selector.build(); + + function addSet(lset) { + var row = limitSetTmpl.cloneNode(true); + row.setAttribute('limit_set', lset); + byName('limit-set', row).innerHTML = limitSetCache[lset].name(); + byName('remove-limit-set', row).onclick = function() { + byName('limit-set-entry-tbody', cmGrid.editPane.domNode).removeChild(row); + } + byName('limit-set-active', row).setAttribute('checked', 'true'); + byName('limit-set-entry-tbody', editPane.domNode).appendChild(row); + } - byName('add-circ-mod-group', node).onclick = function() { - addCircModGroup(node, tableTmpl) + byName('add-limit-set', editPane.domNode).onclick = function() { + addSet(selector.widget.attr('value')); } + // On edit we need to load existing entries. + // On create, not so much. + if(!editPane.fmObject) return; + var matchpoint = editPane.fmObject.id(); + if(editPane.mode == 'update') { - var groups = new openils.PermaCrud().search('ccmcmt', {matchpoint: editPane.fmObject.id()}); - dojo.forEach(groups, function(g) { addCircModGroup(node, tableTmpl, g); } ); + var pcrud = new openils.PermaCrud(); + limitSetEntryCache = pcrud.search('ccmlsm', {matchpoint: editPane.fmObject.id()}); + dojo.forEach(limitSetEntryCache, function(g) { addLimitSet(limitSetTmpl, g); } ); } - - editPane.domNode.appendChild(node); } - -function addCircModGroup(node, tableTmpl, group) { - - var table = tableTmpl.cloneNode(true); - var circModRowTmpl = byName('circ-mod-entry-tbody', table).removeChild(byName('circ-mod-entry-row', table)); - circModGroupTables.push(table); - - var entries = []; - if(group) { - entries = new openils.PermaCrud().search('ccmcmtm', {circ_mod_test : group.id()}); - table.setAttribute('group', group.id()); - circModGroupCache[group.id()] = group; - circModEntryCache[group.id()] = entries; +function addLimitSet(tmpl, limit_set_entry) { + var row = tmpl.cloneNode(true); + var lset = limit_set_entry.limit_set(); + row.setAttribute('limit_set', lset); + byName('limit-set', row).innerHTML = limitSetCache[lset].name(); + if(limit_set_entry.active() == 't') { + byName('limit-set-active', row).setAttribute('checked', 'true'); } - - function addMod(code, name) { - name = name || code; // XXX - var row = circModRowTmpl.cloneNode(true); - byName('circ-mod', row).innerHTML = name; - byName('circ-mod', row).setAttribute('code', code); - byName('circ-mod-entry-tbody', table).appendChild(row); - byName('remove-circ-mod', row).onclick = function() { - byName('circ-mod-entry-tbody', table).removeChild(row); - } + if(limit_set_entry.fallthrough() == 't') { + byName('limit-set-fallthrough', row).setAttribute('checked', 'true'); } - - dojo.forEach(entries, function(e) { addMod(e.circ_mod()); }); - - byName('circ-mod-count', table).value = (group) ? group.items_out() : 0; - - var selector = new openils.widget.AutoFieldWidget({ - fmClass : 'ccmcmtm', - fmField : 'circ_mod', - parentNode : byName('circ-mod-selector', table) - }); - selector.build(); - - byName('add-circ-mod', table).onclick = function() { - addMod(selector.widget.attr('value'), selector.widget.attr('displayedValue')); + byName('remove-limit-set', row).onclick = function() { + byName('limit-set-entry-tbody', cmGrid.editPane.domNode).removeChild(row); } + byName('limit-set-entry-tbody', cmGrid.editPane.domNode).appendChild(row); +} - node.insertBefore(table, byName('add-circ-mod-group-span', node)); - node.insertBefore(dojo.create('hr'), byName('add-circ-mod-group-span', node)); +function format_hard_due_date(name, id) { + var item=this.grid.getItem(id); + if(!item) return name; + switch (this.grid.store.getValue(this.grid.getItem(id), 'hard_due_date')) { + case null : + case undefined : + case 'unset' : + return name; + default: + return "" + name + ""; + } } -function applyCircModChanges() { +function updateLinked(fmObject, rowindex) { + var id = null; + if(rowindex != undefined && this.editPane && this.editPane.fmObject) { + // Edit, grab existing ID + id = this.editPane.fmObject.id(); + } else if(fmObject.id) { + // Create, grab new ID + id = fmObject.id(); + } + // If we don't have an ID, drop out. + if(id == null) return; var pcrud = new openils.PermaCrud(); progressDialog.show(true); - for(var idx in circModGroupTables) { - var table = circModGroupTables[idx]; - var gp = table.getAttribute('group'); - - var count = byName('circ-mod-count', table).value; - var mods = []; - var entries = []; - - dojo.forEach(dojo.query('[name=circ-mod]', table), function(td) { - mods.push(td.getAttribute('code')); - }); - - var group = circModGroupCache[gp]; - - if(!group) { - - group = new fieldmapper.ccmcmt(); - group.isnew(true); - dojo.forEach(mods, function(mod) { - var entry = new fieldmapper.ccmcmtm(); + var add = []; + var remove = []; + var update = []; + + var limit_sets = []; + dojo.query('[name=limit-set-entry-row]', this.editPane.domNode).forEach( + function(row) { + var lset = row.getAttribute('limit_set'); + limit_sets.push(lset); + var cached = limitSetEntryCache.filter(function(i) { return (i.limit_set() == lset); })[0]; + if(!cached) { + var entry = new fieldmapper.ccmlsm(); entry.isnew(true); - entry.circ_mod(mod); - entries.push(entry); - }); - - - } else { - - var existing = circModEntryCache[group.id()]; - dojo.forEach(mods, function(mod) { - - // new circ mod for this group - if(!existing.filter(function(i){ return (i.circ_mod() == mod)})[0]) { - var entry = new fieldmapper.ccmcmtm(); - entry.isnew(true); - entry.circ_mod(mod); - entries.push(entry); - entry.circ_mod_test(group.id()); + entry.matchpoint(id); + entry.limit_set(lset); + entry.active(byName('limit-set-active', row).checked ? 't' : 'f'); + entry.fallthrough(byName('limit-set-fallthrough', row).checked ? 't' : 'f'); + add.push(entry); + } else { + var active = byName('limit-set-active', row).checked; + var fallthrough = byName('limit-set-fallthrough', row).checked; + if((active != (cached.active() == 't')) || (fallthrough != (cached.fallthrough() == 't'))) { + cached.active(active ? 't' : 'f'); + cached.fallthrough(fallthrough ? 't' : 'f'); + cached.ischanged(true); + update.push(cached); } - }); - - dojo.forEach(existing, function(eMod) { - if(!mods.filter(function(i){ return (i == eMod.circ_mod()) })[0]) { - eMod.isdeleted(true); - entries.push(eMod); - } - }); + } } + ); + dojo.forEach(limitSetEntryCache, function(eSet) { + if(!limit_sets.filter(function(i) { return (i == eSet.limit_set()); })[0]) { + eSet.isdeleted(true); + remove.push(eSet); + } + } + ); - group.items_out(count); - group.matchpoint(matchPoint); - - if(group.isnew()) { + function updateEntries() { + pcrud.update(update, { + oncomplete : function () { + progressDialog.hide(); + } + }); + } - pcrud.create(group, { - oncomplete : function(r, cudResults) { - var group = cudResults[0]; - dojo.forEach(entries, function(e) { e.circ_mod_test(group.id()) } ); - pcrud.create(entries, { - oncomplete : function() { - progressDialog.hide(); - } - }); + function removeEntries() { + pcrud.eliminate(remove, { + oncomplete : function () { + if(update.length) { + updateEntries(); + } else { + progressDialog.hide(); } - }); - - } else { - - pcrud.update(group, { - oncomplete : function(r, cudResults) { - var newOnes = entries.filter(function(e) { return e.isnew() }); - var delOnes = entries.filter(function(e) { return e.isdeleted() }); - if(!delOnes.length && !newOnes.length) { - progressDialog.hide(); - return; - } - if(newOnes.length) { - pcrud.create(newOnes, { - oncomplete : function() { - if(delOnes.length) { - pcrud.eliminate(delOnes, { - oncomplete : function() { - progressDialog.hide(); - } - }); - } else { - progressDialog.hide(); - } - } - }); - } else { - pcrud.eliminate(delOnes, { - oncomplete : function() { - progressDialog.hide(); - } - }); - } + } + }); + } + + function addEntries() { + pcrud.create(add, { + oncomplete : function () { + if(remove.length) { + removeEntries(); + } else if (update.length) { + updateEntries(); + } else { + progressDialog.hide(); } - }); - } + } + }); } + + if(add.length) + addEntries(); + else if (remove.length) + removeEntries(); + else if (update.length) + updateEntries(); + else + progressDialog.hide(); } openils.Util.addOnLoad(load); diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index fc327f6f21..2acf0c759f 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -716,6 +716,7 @@ + @@ -746,6 +747,7 @@ + diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js index 402acb0e3c..e84ffac01e 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js @@ -770,6 +770,10 @@ main.menu.prototype = { ['oncommand'], function(event) { open_eg_web_page('conify/global/permission/grp_penalty_threshold', null, event); } ], + 'cmd_local_admin_circ_limit_set' : [ + ['oncommand'], + function(event) { open_eg_web_page('conify/global/config/circ_limit_set', null, event); } + ], 'cmd_server_admin_config_rule_circ_duration' : [ ['oncommand'], function(event) { open_eg_web_page('conify/global/config/rule_circ_duration', null, event); } @@ -810,6 +814,10 @@ main.menu.prototype = { ['oncommand'], function(event) { open_eg_web_page('conify/global/config/asset_sip_fields', null, event); } ], + 'cmd_server_admin_circ_limit_group' : [ + ['oncommand'], + function(event) { open_eg_web_page('conify/global/config/circ_limit_group', null, event); } + ], 'cmd_local_admin_external_text_editor' : [ ['oncommand'], function() { diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul index dd1f479cee..c12c2455fc 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul +++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul @@ -149,7 +149,9 @@ + @@ -195,6 +197,9 @@ + @@ -485,6 +490,7 @@ + @@ -522,6 +528,7 @@ +