From a4c9251f5e55fb37ef7628311a27bad91fb39b83 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Tue, 25 Feb 2025 17:42:47 +0000 Subject: [PATCH] SK/FB work --- Civi/ContactCats/Processor.php | 44 +++++-- ang/afsearchContactsByCategory.aff.html | 8 +- ang/afsearchContactsByCategory.aff.json | 28 ++--- ang/crmContactcats.js | 18 ++- .../crmContactCategorySettings.html | 110 +++++++++--------- ...tionValue_changed_contact_category.mgd.php | 24 ---- managed/searchkit-category-counts.mgd.php | 83 ------------- .../searchkit-contacts-by-category.mgd.php | 64 ++++------ schema/ContactCategory.entityType.php | 14 ++- .../ContactCategoryDefinition.entityType.php | 3 + 10 files changed, 163 insertions(+), 233 deletions(-) delete mode 100644 managed/OptionValue_changed_contact_category.mgd.php delete mode 100644 managed/searchkit-category-counts.mgd.php diff --git a/Civi/ContactCats/Processor.php b/Civi/ContactCats/Processor.php index b6a3c90..34effd7 100644 --- a/Civi/ContactCats/Processor.php +++ b/Civi/ContactCats/Processor.php @@ -24,6 +24,11 @@ class Processor { ->addOrderBy('execution_order') ->execute()->getArrayCopy(); + // Check the last item is the 'default' one. + if ((end($this->categories)['search_type'] ?? '') !== 'default') { + throw new CRM_Core_Exception("ContactCategoryDefinition unconfigured; no default category."); + } + // Identify groups and searches used by definitions. $groupIDs = $searchIDs = []; foreach ($this->categories as $cat) { @@ -72,8 +77,10 @@ class Processor { // TODO: check things like searches that group by contact_id } + /** + * Main calling point. + */ public function run() { - $this->refreshSmartGroups(); $summary = []; \CRM_Core_Transaction::create()->run(function($tx) use (&$summary) { @@ -85,6 +92,12 @@ class Processor { elseif ($cat['search_type'] === 'search') { $this->assignCategoryFromSearch($cat); } + elseif ($cat['search_type'] === 'default') { + $this->assignDefaultCategory($cat); + } + else { + throw new CRM_Core_DAO("Invalid search_type: $cat[search_type] for category $cat[id]"); + } // future... } $summary = $this->createChangeActivities(); @@ -96,10 +109,10 @@ class Processor { protected function createChangeActivities() { Civi::log()->debug("Calculate changes", ['=' => 'timed', '=start' => "changes"]); $changes = CRM_Core_DAO::executeQuery(<< category - ORDER BY category, next_category + WHERE next_category <> category_definition_id + ORDER BY category_definition_id, next_category SQL); $lastChange = [0, 0]; $batch = []; @@ -147,10 +160,11 @@ class Processor { Civi::log()->debug('Apply changes', ['=change' => 'applyChanges', '=timed' => 1]); CRM_Core_DAO::executeQuery("UPDATE civicrm_contact_category - SET category = next_category WHERE category <> next_category")->free(); + SET category_definition_id = next_category + WHERE category_definition_id IS NULL OR category_definition_id <> next_category")->free(); Civi::log()->debug('', ['=pop' => 1]); - $summary = CRM_Core_DAO::executeQuery("SELECT next_category, count(*) from civicrm_contact_category group by next_category")->fetchAll(); + $summary = CRM_Core_DAO::executeQuery("SELECT next_category, count(*) from civicrm_contact_category GROUP BY next_category")->fetchAll(); $summary['changes'] = $n; $_ = memory_get_peak_usage(TRUE); $summary['memory_use'] = @round($_ / pow(1024, ($i = floor(log($_, 1024)))), 2) . ' ' . ['b', 'kb', 'mb', 'gb', 'tb', 'pb'][$i]; @@ -176,12 +190,14 @@ class Processor { Civi::log()->debug('Resetting table', ['=' => 'start,timed']); // clear out temp space. CRM_Core_DAO::executeQuery("UPDATE civicrm_contact_category SET next_category = 0;")->free(); + Civi::log()->debug('Resetting table stage 2'); // ensure we have all our contacts covered. - // TODO: is it quicker to do a WHERE NOT EXISTS? + // Q: is it quicker to do a WHERE NOT EXISTS? A: nope. CRM_Core_DAO::executeQuery(<<free(); Civi::log()->debug('', ['=' => 'pop']); } @@ -205,6 +221,7 @@ class Processor { WHERE next_category = 0 SQL; CRM_Core_DAO::executeQuery($sql)->free(); + Civi::log()->debug('', ['=' => 'pop']); } /** @@ -212,6 +229,7 @@ class Processor { */ protected function assignCategoryFromSearch(array $cat) { $search = $this->searchDetails[$cat['search_data']['saved_search_id']]; + Civi::log()->debug("Doing $cat[id] $cat[label]", ['=' => 'timed', '=start' => "ss" . $cat['search_data']['saved_search_id']]); $apiParams = $search['api_params']; if ($search['api_entity'] === 'Contact' && in_array('id', $apiParams['select'] ?? [])) { @@ -225,7 +243,7 @@ class Processor { // We don't need them ordered. unset($apiParams['orderBy']); - $contactIDs = civicrm_api4($search['api_entity'], $search['get'], $apiParams)->column($contactIdKey); + $contactIDs = civicrm_api4($search['api_entity'], 'get', $apiParams)->column($contactIdKey); // Unsure if this batching is needed $batchSize = 10000; while ($batch = array_splice($contactIDs, 0, $batchSize)) { @@ -241,6 +259,12 @@ class Processor { SQL; CRM_Core_DAO::executeQuery($sql)->free(); } + Civi::log()->debug('', ['=' => 'pop']); + } + + protected function assignDefaultCategory(array $cat) { + $id = (int) $cat['id']; + CRM_Core_DAO::executeQuery("UPDATE civicrm_contact_category SET next_category = $id WHERE next_category = 0;")->free(); } } diff --git a/ang/afsearchContactsByCategory.aff.html b/ang/afsearchContactsByCategory.aff.html index 275a7d2..b07b107 100644 --- a/ang/afsearchContactsByCategory.aff.html +++ b/ang/afsearchContactsByCategory.aff.html @@ -1,5 +1,7 @@ -
- - +
+
+ + +
diff --git a/ang/afsearchContactsByCategory.aff.json b/ang/afsearchContactsByCategory.aff.json index 137eafd..dccb86a 100644 --- a/ang/afsearchContactsByCategory.aff.json +++ b/ang/afsearchContactsByCategory.aff.json @@ -1,31 +1,31 @@ { "type": "search", + "requires": null, + "entity_type": null, + "join_entity": null, "title": "Contacts by category", "description": "Individuals only", "placement": [], + "summary_contact_type": null, + "summary_weight": null, "icon": "fa-tags", "server_route": "civicrm/contact/search/category", + "is_public": false, "permission": [ "access CiviCRM" ], "permission_operator": "AND", + "redirect": null, + "submit_enabled": true, + "submit_limit": null, + "create_submission": false, + "manual_processing": false, + "allow_verification_by_email": false, + "email_confirmation_template_id": null, "navigation": { "parent": "Search", "label": "Contacts by category", "weight": 0 }, - "modified_date": "2024-02-28 16:59:26", - "requires": null, - "entity_type": null, - "join_entity": null, - "summary_contact_type": null, - "summary_weight": null, - "is_public": false, - "redirect": null, - "submit_enabled": true, - "submit_limit": null, - "create_submission": null, - "manual_processing": null, - "allow_verification_by_email": null, - "email_confirmation_template_id": null + "modified_date": "2025-02-25 14:51:17" } diff --git a/ang/crmContactcats.js b/ang/crmContactcats.js index 3bb4503..52bee2c 100644 --- a/ang/crmContactcats.js +++ b/ang/crmContactcats.js @@ -26,7 +26,21 @@ ctrl.moveIdx = null; ctrl.categoryToEdit = null; ctrl.categoryDefinitions = null; - ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { label: 'ASC' }, withLabels: true }); + ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { execution_order: 'ASC' }, withLabels: true }); + // Ensure we have the minimum of what we need. + if (!ctrl.categoryDefinitions.find(c => c.search_type === 'default')) { + // There's no default one. + ctrl.categoryDefinitions.push({ + id: 0, + execution_order: ctrl.categoryDefinitions.length, + label: '9999 ' + ts('Other'), + search_type: 'default', + search_data: {}, + color: '#666666', + icon: 'fa-person-circle-question', + description: ts('Any contact not matching any of the categories is assigned this category.'), + }) + } updateOrders(); console.log("Got", ctrl.categoryDefinitions); @@ -89,7 +103,7 @@ ctrl.categoryDefinitions.splice(idx, 0, item); ctrl.moveIdx = null; updateOrders(); - $ctrl.dirty = 'dirty'; + ctrl.dirty = 'dirty'; }; ctrl.deleteCategory = idx => { diff --git a/ang/crmContactcats/crmContactCategorySettings.html b/ang/crmContactcats/crmContactCategorySettings.html index 30a8d1b..83f88f0 100644 --- a/ang/crmContactcats/crmContactCategorySettings.html +++ b/ang/crmContactcats/crmContactCategorySettings.html @@ -36,7 +36,7 @@ - - +
@@ -102,66 +102,66 @@ /> -
- -
+
+ +
-
- -
+
+ + +
-
- -
+
+ +
-
- -
+
+ +
- -
- -
+ +
+ +
- -
- -
+ +
+ +
- - - - + + + diff --git a/managed/OptionValue_changed_contact_category.mgd.php b/managed/OptionValue_changed_contact_category.mgd.php deleted file mode 100644 index 1be1df0..0000000 --- a/managed/OptionValue_changed_contact_category.mgd.php +++ /dev/null @@ -1,24 +0,0 @@ - 'OptionValue_changed_contact_category', - 'entity' => 'OptionValue', - 'cleanup' => 'unused', - 'update' => 'unmodified', - 'params' => [ - 'version' => 4, - 'values' => [ - 'option_group_id.name' => 'activity_type', - 'label' => E::ts('Changed Contact Category'), - 'name' => 'changed_contact_category', - 'icon' => 'fa-tags', - ], - 'match' => [ - 'option_group_id', - 'name', - ], - ], - ], -]; diff --git a/managed/searchkit-category-counts.mgd.php b/managed/searchkit-category-counts.mgd.php deleted file mode 100644 index ee04bb0..0000000 --- a/managed/searchkit-category-counts.mgd.php +++ /dev/null @@ -1,83 +0,0 @@ - 'SavedSearch_Contact_Category_Summary', - 'entity' => 'SavedSearch', - 'cleanup' => 'always', - 'update' => 'unmodified', - 'params' => [ - 'version' => 4, - 'values' => [ - 'name' => 'Contact_Category_Summary', - 'label' => E::ts('Contact Category Summary'), - 'api_entity' => 'ContactCategory', - 'api_params' => [ - 'version' => 4, - 'select' => [ - 'category:label', - 'COUNT(contact_id) AS COUNT_contact_id', - ], - 'orderBy' => [], - 'where' => [], - 'groupBy' => [ - 'category', - ], - 'join' => [], - 'having' => [], - ], - ], - 'match' => [ - 'name', - ], - ], - ], -[ - 'name' => 'SavedSearch_Contact_Category_Summary_SearchDisplay_Contact_Category_Summary', - 'entity' => 'SearchDisplay', - 'cleanup' => 'always', - 'update' => 'unmodified', - 'params' => [ - 'version' => 4, - 'values' => [ - 'name' => 'Contact_Category_Summary', - 'label' => E::ts('Contact Category Summary'), - 'saved_search_id.name' => 'Contact_Category_Summary', - 'type' => 'table', - 'settings' => [ - 'description' => NULL, - 'sort' => [], - 'limit' => 0, - 'pager' => FALSE, - 'placeholder' => 5, - 'columns' => [ - [ - 'type' => 'field', - 'key' => 'category:label', - 'dataType' => 'Integer', - 'label' => E::ts('Category'), - 'sortable' => TRUE, - ], - [ - 'type' => 'field', - 'key' => 'COUNT_contact_id', - 'dataType' => 'Integer', - 'label' => E::ts('Count'), - 'sortable' => TRUE, - ], - ], - 'actions' => TRUE, - 'classes' => [ - 'table', - 'table-striped', - ], - ], - ], - 'match' => [ - 'saved_search_id', - 'name', - ], - ], -], -]; diff --git a/managed/searchkit-contacts-by-category.mgd.php b/managed/searchkit-contacts-by-category.mgd.php index fc15d74..bb0f268 100644 --- a/managed/searchkit-contacts-by-category.mgd.php +++ b/managed/searchkit-contacts-by-category.mgd.php @@ -19,11 +19,7 @@ return [ 'id', 'contact_sub_type:label', 'display_name', - 'Contact_ContactCategory_contact_id_01.category:label', - 'Synopsis_Fields.Amount_of_last_contribution', - 'Synopsis_Fields.Date_of_Last_Contribution', - 'Synopsis_Fields.Total_Lifetime_Contributions', - 'Synopsis_Fields.Total_count_of_contributions', + 'Contact_ContactCategory_contact_id_01_ContactCategory_ContactCategoryDefinition_category_definition_id_01.label', ], 'orderBy' => [], 'where' => [ @@ -43,6 +39,25 @@ return [ '=', 'Contact_ContactCategory_contact_id_01.contact_id', ], + [ + 'Contact_ContactCategory_contact_id_01.id', + '=', + 'id', + ], + ], + [ + 'ContactCategoryDefinition AS Contact_ContactCategory_contact_id_01_ContactCategory_ContactCategoryDefinition_category_definition_id_01', + 'INNER', + [ + 'Contact_ContactCategory_contact_id_01.category_definition_id', + '=', + 'Contact_ContactCategory_contact_id_01_ContactCategory_ContactCategoryDefinition_category_definition_id_01.id', + ], + [ + 'Contact_ContactCategory_contact_id_01_ContactCategory_ContactCategoryDefinition_category_definition_id_01.id', + '=', + 'Contact_ContactCategory_contact_id_01.category_definition_id', + ], ], ], 'having' => [], @@ -77,13 +92,6 @@ return [ 'pager' => [], 'placeholder' => 5, 'columns' => [ - [ - 'type' => 'field', - 'key' => 'Contact_ContactCategory_contact_id_01.category:label', - 'dataType' => 'Integer', - 'label' => E::ts('Category'), - 'sortable' => TRUE, - ], [ 'type' => 'field', 'key' => 'display_name', @@ -108,34 +116,10 @@ return [ ], [ 'type' => 'field', - 'key' => 'Synopsis_Fields.Total_Lifetime_Contributions', - 'dataType' => 'Money', - 'label' => E::ts('£ total'), + 'key' => 'Contact_ContactCategory_contact_id_01_ContactCategory_ContactCategoryDefinition_category_definition_id_01.label', + 'dataType' => 'String', + 'label' => E::ts('Category'), 'sortable' => TRUE, - 'alignment' => 'text-right', - ], - [ - 'type' => 'field', - 'key' => 'Synopsis_Fields.Total_count_of_contributions', - 'dataType' => 'Integer', - 'label' => E::ts('Count donations'), - 'sortable' => TRUE, - 'alignment' => 'text-right', - ], - [ - 'type' => 'field', - 'key' => 'Synopsis_Fields.Date_of_Last_Contribution', - 'dataType' => 'Timestamp', - 'label' => E::ts('Latest donation'), - 'sortable' => TRUE, - ], - [ - 'type' => 'field', - 'key' => 'Synopsis_Fields.Amount_of_last_contribution', - 'dataType' => 'Money', - 'label' => E::ts('£ latest'), - 'sortable' => TRUE, - 'alignment' => 'text-right', ], ], 'actions' => TRUE, @@ -143,9 +127,11 @@ return [ 'table-striped', ], 'headerCount' => TRUE, + 'actions_display_mode' => 'menu', ], ], 'match' => [ + 'saved_search_id', 'name', ], ], diff --git a/schema/ContactCategory.entityType.php b/schema/ContactCategory.entityType.php index fc9dadd..259b556 100644 --- a/schema/ContactCategory.entityType.php +++ b/schema/ContactCategory.entityType.php @@ -46,14 +46,22 @@ return [ 'category_definition_id' => [ 'title' => E::ts('Category Definition'), 'sql_type' => 'int unsigned', - 'required' => TRUE, - 'default' => 0, - 'input_type' => 'EntityRef', + 'required' => FALSE, + 'default' => NULL, + // 'input_type' => 'EntityRef', + 'input_type' => 'Select', 'entity_reference' => [ 'entity' => 'ContactCategoryDefinition', 'key' => 'id', 'on_delete' => 'CASCADE', ], + 'pseudoconstant' => [ + 'prefetch' => TRUE, + 'table' => 'civicrm_contact_category_definition', + 'key_column' => 'id', + 'label_column' => 'label', + 'name_column' => 'id', + ], ], 'next_category' => [ 'title' => E::ts('Next Category'), diff --git a/schema/ContactCategoryDefinition.entityType.php b/schema/ContactCategoryDefinition.entityType.php index 25ca278..2427abc 100644 --- a/schema/ContactCategoryDefinition.entityType.php +++ b/schema/ContactCategoryDefinition.entityType.php @@ -10,6 +10,8 @@ return [ 'title_plural' => E::ts('Contact Category Definitions'), 'description' => E::ts('Holds definition of a "Contact category"'), 'log' => FALSE, + 'label_field' => 'label', + 'icon' => 'fa-tags', ], 'getFields' => fn() => [ 'id' => [ @@ -42,6 +44,7 @@ return [ // 'name' => 'title', 'search' => E::ts('Search Kit search'), 'group' => E::ts('Group'), + 'default' => E::ts('Default category'), // Future: // 'sql' => E::ts('SQL template'), ],