From 205987b28f5d71761241a14ce252e0b5b2a8430c Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 25 Feb 2026 13:46:40 +0000 Subject: [PATCH] Bug 10190: remove has_rules sentinel from circulation_rules table has_rules was a purely frontend marker stored as a circulation rule to signal that a context had explicitly-saved overdue trigger rules. The backend never reads it; the information is fully derivable from whether any of delay/notice/mtt/restrict are non-null for a given context. Replace all has_rules DB reads/writes with a hasExplicitRulesForTrigger() helper that checks the four real rule suffixes directly. has_rules remains as a computed in-memory concept in the Vue store (derived in findEffectiveRule) so templates require no changes. Also adds an atomicupdate to delete existing has_rules rows from the DB, and removes the has_rules insert from the bug_10190 migration script. --- Koha/CirculationRules.pm | 4 -- .../data/mysql/atomicupdate/bug_10190.pl | 4 -- .../CirculationTriggersFormAdd.vue | 2 - ...lationTriggersFormConfirmTriggerDelete.vue | 14 +++- .../prog/js/vue/stores/circulation-rules.js | 67 ++++++++++++------- 5 files changed, 52 insertions(+), 39 deletions(-) diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm index ef67e367f37..8e6b4df3ef5 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -194,10 +194,6 @@ our $RULE_KINDS = { scope => [ 'branchcode', 'categorycode', 'itemtype' ], can_be_blank => 0, }, - overdue_X_has_rules => { - scope => [ 'branchcode', 'categorycode', 'itemtype' ], - can_be_blank => 0, - }, renewalperiod => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], }, diff --git a/installer/data/mysql/atomicupdate/bug_10190.pl b/installer/data/mysql/atomicupdate/bug_10190.pl index 39e0cf5d8c8..aabdb48a38d 100755 --- a/installer/data/mysql/atomicupdate/bug_10190.pl +++ b/installer/data/mysql/atomicupdate/bug_10190.pl @@ -113,10 +113,6 @@ return { $insert->execute( $branchcode, $categorycode, $itemtype, "overdue_${i}_restrict", $restrict ); } - # has_rules is a UI sentinel that marks an explicit ruleset for this context/trigger. - # It is not used by the backend (overdue_notices.pl). - $insert->execute( $branchcode, $categorycode, $itemtype, "overdue_${i}_has_rules", 1 ); - $migrated++; } } diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue index 7cb9efb70df..44c8894e896 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue @@ -599,8 +599,6 @@ export default { ); } - ruleSetToSubmit[`overdue_${this.triggerNumber}_has_rules`] = true; - // in edit mode, check for changes from elsewhere to the rule set being edited if (this.editMode === "edit") { const ruleSetInDb = await this.getSelectedRuleSet(this.context); diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue index ccc989f754a..38aa1b8f507 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue @@ -49,8 +49,12 @@ import { storeToRefs } from "pinia"; export default { setup() { const circRulesStore = inject("circRulesStore"); - const { handleContext, findEffectiveRule, deleteRuleSet } = - circRulesStore; + const { + handleContext, + findEffectiveRule, + deleteRuleSet, + hasExplicitRulesForTrigger, + } = circRulesStore; const { libraries, allCurrentLibraryRawRuleSets, ruleSuffixes } = storeToRefs(circRulesStore); return { @@ -60,6 +64,7 @@ export default { allCurrentLibraryRawRuleSets, ruleSuffixes, deleteRuleSet, + hasExplicitRulesForTrigger, }; }, data() { @@ -108,7 +113,10 @@ export default { }; if ( - ruleSet[`overdue_${this.triggerNumber}_has_rules`] === null + !this.hasExplicitRulesForTrigger( + ruleSet, + this.triggerNumber + ) ) { return; } diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js index 5129a704107..27ad9875cde 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js @@ -32,7 +32,6 @@ export const useCircRulesStore = defineStore("circRules", () => { { code: "sms", name: "SMS" }, { code: "print", name: "Print" }, ], - regex: /overdue_(\d+)_has_rules/, // rule sets allDefaultLibraryRawRuleSets: [], // source of truth for default library allCurrentLibraryRawRuleSets: [], // source of truth for current library @@ -97,10 +96,15 @@ export const useCircRulesStore = defineStore("circRules", () => { } return value.includes(type) ? $__("Yes") : $__("No"); }, + hasExplicitRulesForTrigger(ruleSet, triggerNumber) { + return ["delay", "notice", "mtt", "restrict"].some( + suffix => ruleSet[`overdue_${triggerNumber}_${suffix}`] != null + ); + }, hasConflict(oldRuleSet, newRuleSet, triggerNumber) { if ( !oldRuleSet || - oldRuleSet[`overdue_${triggerNumber}_has_rules`] === null + !this.hasExplicitRulesForTrigger(oldRuleSet, triggerNumber) ) { return false; } @@ -125,8 +129,8 @@ export const useCircRulesStore = defineStore("circRules", () => { }, isOnlyRuleSetForTrigger(triggerNumber) { return ( - this.allCurrentLibraryRawRuleSets.filter( - ruleSet => ruleSet[`overdue_${triggerNumber}_has_rules`] + this.allCurrentLibraryRawRuleSets.filter(ruleSet => + this.hasExplicitRulesForTrigger(ruleSet, triggerNumber) ).length === 1 ); }, @@ -163,12 +167,20 @@ export const useCircRulesStore = defineStore("circRules", () => { ruleSet?.context.item_type_id === context.item_type_id ); - // if handling 'has_rules', stop here + // if handling 'has_rules', derive from actual rules rather than DB if (ruleSuffix === "has_rules") { + const hasExplicit = this.currentAndDefaultRawRuleSets.some( + ruleSet => + ruleSet?.context.library_id === context.library_id && + ruleSet?.context.patron_category_id === + context.patron_category_id && + ruleSet?.context.item_type_id === + context.item_type_id && + this.hasExplicitRulesForTrigger(ruleSet, triggerNumber) + ); return { - value: existingRule?.[`overdue_${triggerNumber}_has_rules`], - isFallback: - !existingRule?.[`overdue_${triggerNumber}_has_rules`], + value: hasExplicit ? true : null, + isFallback: !hasExplicit, }; } @@ -178,8 +190,10 @@ export const useCircRulesStore = defineStore("circRules", () => { value: existingRule[ `overdue_${triggerNumber}_${ruleSuffix}` ], - isFallback: - !existingRule[`overdue_${triggerNumber}_has_rules`], + isFallback: !this.hasExplicitRulesForTrigger( + existingRule, + triggerNumber + ), }; } @@ -289,7 +303,7 @@ export const useCircRulesStore = defineStore("circRules", () => { i <= this.triggerCounts[this.currentLibraryId]; i++ ) { - if (ruleSet[`overdue_${i}_has_rules`] === null) { + if (!this.hasExplicitRulesForTrigger(ruleSet, i)) { continue; } this.ruleSuffixes.forEach(ruleSuffix => { @@ -326,18 +340,23 @@ export const useCircRulesStore = defineStore("circRules", () => { // - Rule sets exists that override default triggers. Therefore, their triggerCount is the same as default's. // - Rule sets exists for triggers for which there is no default. // => such triggers are follow up addition to the existing default sequence. - // => the triggerCount for this library will be higher than default's, and equal to the highest trigger number for this library for which has_rules is not null. + // => the triggerCount for this library will be higher than default's, and equal to the highest trigger number for this library that has any explicit rules. - const ruleNames = Object.keys(this.allDefaultLibraryRawRuleSets[0]); // Set the triggerCount for the default library rule set if (this.currentLibraryId === "*") { - this.triggerCounts["*"] = ruleNames.filter( - ruleSuffix => - this.regex.test(ruleSuffix) && - this.allDefaultLibraryRawRuleSets.some( - ruleSet => ruleSet[ruleSuffix] !== null - ) - ).length; + const triggerNumRegex = + /^overdue_(\d+)_(delay|notice|mtt|restrict)$/; + const triggerNums = new Set(); + this.allDefaultLibraryRawRuleSets.forEach(ruleSet => { + Object.keys(ruleSet).forEach(key => { + const match = key.match(triggerNumRegex); + if (match && ruleSet[key] !== null) { + triggerNums.add(parseInt(match[1])); + } + }); + }); + this.triggerCounts["*"] = + triggerNums.size > 0 ? Math.max(...triggerNums) : 0; return; } @@ -351,9 +370,8 @@ export const useCircRulesStore = defineStore("circRules", () => { // Set a library-specific trigger count: at least one rule set exists -> start from the first trigger for which there is no default rule set let i = this.triggerCounts["*"] + 1; while ( - ruleNames.includes(`overdue_${i}_has_rules`) && - this.allCurrentLibraryRawRuleSets.some( - ruleSet => ruleSet[`overdue_${i}_has_rules`] !== null + this.allCurrentLibraryRawRuleSets.some(ruleSet => + this.hasExplicitRulesForTrigger(ruleSet, i) ) ) { i++; @@ -391,7 +409,6 @@ export const useCircRulesStore = defineStore("circRules", () => { if (ruleSet[`overdue_${triggerNumber}_mtt`] !== null) { rulesForDeletion[`overdue_${triggerNumber}_mtt`] = null; } - rulesForDeletion[`overdue_${triggerNumber}_has_rules`] = null; this.updateCircRuleSets(rulesForDeletion, triggerNumber); }, async getAllRawRuleSets() { @@ -473,8 +490,6 @@ export const useCircRulesStore = defineStore("circRules", () => { existingRuleSet[`overdue_${triggerNumber}_restrict`]; circRuleSet[`overdue_${triggerNumber}_mtt`] = existingRuleSet[`overdue_${triggerNumber}_mtt`]; - circRuleSet[`overdue_${triggerNumber}_has_rules`] = - existingRuleSet[`overdue_${triggerNumber}_has_rules`]; const client = APIClient.circRule; await client.circ_rules.update(circRuleSet); }, -- 2.53.0