diff --git a/CHANGELOG.md b/CHANGELOG.md index 3746242..3d35fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Fixed + +- Improve performance of the consumed details, the ticket Credit tab and the entity Credit tab, which no longer run SQL queries for each displayed row +- Remove 2 SQL queries from every GLPI page by dropping the plugin's own activation check +- Delete the default voucher options of a ticket when it is purged + ## [1.15.8] - 2026-10-27 ### Fixed diff --git a/inc/entity.class.php b/inc/entity.class.php index 0be59d8..cdd2d86 100644 --- a/inc/entity.class.php +++ b/inc/entity.class.php @@ -208,21 +208,30 @@ public static function showForItemtype(Entity $entity, $itemtype = 'Entity') ]; } + $vouchers = self::getAllForEntity($ID, $sqlfilter); + $consumed = self::getConsumedForCredits(array_keys($vouchers)); + $type_links = []; + $entries = []; - foreach (self::getAllForEntity($ID, $sqlfilter) as $data) { + foreach ($vouchers as $data) { $quantity_sold = (int) $data['quantity']; if (0 === $quantity_sold) { $quantity_sold = __('Unlimited'); } + // The row already holds every field getById() would load. $item = new self(); - $item = $item->getById($data['id']); + $item->fields = $data; + $consumed_for_credit = $consumed[$data['id']] ?? 0; if (!empty($data['plugin_credit_types_id'])) { - $type = new PluginCreditType(); - $type = $type->getById($data['plugin_credit_types_id']); - if ($type) { - $data['plugin_credit_types_id'] = $type->getLink(); + $types_id = $data['plugin_credit_types_id']; + if (!array_key_exists($types_id, $type_links)) { + $type = PluginCreditType::getById((int) $types_id); + $type_links[$types_id] = $type ? $type->getLink() : null; + } + if ($type_links[$types_id] !== null) { + $data['plugin_credit_types_id'] = $type_links[$types_id]; } } else { $data['plugin_credit_types_id'] = ''; @@ -238,7 +247,7 @@ public static function showForItemtype(Entity $entity, $itemtype = 'Entity') ], ); - $link = "" . PluginCreditEntity::getConsumedForCredit($data['id']) . ""; + $link = "" . $consumed_for_credit . ""; $entries[] = array_merge($data, [ 'name' => $item->getLink(), @@ -246,7 +255,7 @@ public static function showForItemtype(Entity $entity, $itemtype = 'Entity') 'itemtype' => PluginCreditEntity::class, 'low_credit_alert' => $data['low_credit_alert'] == -1 ? __('Disabled') : $data['low_credit_alert'] . '%', 'quantity_consumed' => $modal . $link, - 'quantity_remaining' => $data['quantity'] > 0 ? $data['quantity'] - PluginCreditEntity::getConsumedForCredit($data['id']) : 'Unlimited', + 'quantity_remaining' => $data['quantity'] > 0 ? $data['quantity'] - $consumed_for_credit : 'Unlimited', 'entities_id' => Entity::badgeCompletenameLink($entity), ]); } @@ -674,4 +683,40 @@ public static function getConsumedForCredit(int $credit_id) return (int) $ticket_result['consumed_total']; } + + /** + * Get the total consumption of several credit vouchers in one query. + * + * @param int[] $credit_ids IDs of the credit vouchers + * + * @return array Total consumption by voucher ID, vouchers without consumption are absent + */ + public static function getConsumedForCredits(array $credit_ids): array + { + /** @var DBmysql $DB */ + global $DB; + + if ($credit_ids === []) { + return []; + } + + $iterator = $DB->request([ + 'SELECT' => [ + 'plugin_credit_entities_id', + 'SUM' => 'consumed AS consumed_total', + ], + 'FROM' => 'glpi_plugin_credit_tickets', + 'WHERE' => [ + 'plugin_credit_entities_id' => $credit_ids, + ], + 'GROUPBY' => 'plugin_credit_entities_id', + ]); + + $consumed = []; + foreach ($iterator as $row) { + $consumed[$row['plugin_credit_entities_id']] = (int) $row['consumed_total']; + } + + return $consumed; + } } diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 722973a..7fc60d9 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -77,16 +77,11 @@ public static function getIcon() public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - $nb = $item instanceof CommonDBTM ? self::countForItem($item) : 0; - if ($item instanceof Ticket) { - if ($_SESSION['glpishow_count_on_tabs']) { - return self::createTabEntry(self::getTypeName($nb), $nb); - } else { - return self::getTypeName($nb); - } - } else { - return self::getTypeName($nb); + if ($item instanceof Ticket && $_SESSION['glpishow_count_on_tabs']) { + $nb = self::countForItem($item); + return self::createTabEntry(self::getTypeName($nb), $nb); } + return self::getTypeName(Session::getPluralNumber()); } public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) @@ -232,9 +227,27 @@ public static function showForTicket(Ticket $ticket) ], ]; } - foreach (self::getAllForTicket($ID) as $data) { - $credit_entity = new PluginCreditEntity(); - $credit_entity->getFromDB($data['plugin_credit_entities_id']); + $consumptions = self::getAllForTicket($ID); + + // Voucher names and user links are loaded once, not once per consumption. + $voucher_names = []; + $voucher_ids = array_unique(array_column($consumptions, 'plugin_credit_entities_id')); + if ($voucher_ids !== []) { + $vouchers = $DB->request([ + 'SELECT' => ['id', 'name'], + 'FROM' => PluginCreditEntity::getTable(), + 'WHERE' => ['id' => $voucher_ids], + ]); + foreach ($vouchers as $voucher) { + $voucher_names[$voucher['id']] = $voucher['name']; + } + } + $showuserlink = Session::haveRight('user', READ); + $usernames = []; + + foreach ($consumptions as $data) { + $users_id = $data['users_id']; + $usernames[$users_id] ??= $showuserlink ? getUserLink($users_id) : getUserName($users_id); if (!empty($data['plugin_credit_types_id'])) { $type = new PluginCreditType(); @@ -248,10 +261,12 @@ public static function showForTicket(Ticket $ticket) $entries[] = array_merge($data, [ 'id' => $data['id'], - 'name' => $credit_entity->getName(), + 'name' => (string) ($voucher_names[$data['plugin_credit_entities_id']] ?? '') !== '' + ? $voucher_names[$data['plugin_credit_entities_id']] + : NOT_AVAILABLE, 'plugin_credit_types_id' => $data['plugin_credit_types_id'], 'date_creation' => $data['date_creation'], - 'users_id' => Session::haveRight('user', READ) == true ? getUserLink($data["users_id"]) : getUserName($data["users_id"]), + 'users_id' => $usernames[$users_id], 'consumed' => $data['consumed'], 'itemtype' => PluginCreditTicket::class, ]); @@ -271,10 +286,6 @@ public static function showForTicket(Ticket $ticket) 'entries' => $entries, 'massiveactionparams' => $massiveactionparams ?? [], ]); - - $Entity = new Entity(); - $Entity->getFromDB($ticket->fields['entities_id']); - PluginCreditEntity::showForItemtype($Entity, 'Ticket'); } /** @@ -381,38 +392,86 @@ public static function displayVoucherInTicketProcessingForm($params) */ public static function displayConsumed($ID) { + /** @var DBmysql $DB */ + global $DB; + $consumed_credits = self::getConsumedForCreditEntity($ID); $tickets_data = []; if ($consumed_credits > 0) { - foreach (self::getAllForCreditEntity($ID) as $data) { + // Tickets and categories come with the consumptions, in a single query. + $iterator = $DB->request([ + 'SELECT' => [ + 'glpi_tickets.*', + 'glpi_itilcategories.name AS credit_category_name', + 'glpi_itilcategories.completename AS credit_category_completename', + self::getTable() . '.date_creation AS credit_date_creation', + self::getTable() . '.users_id AS credit_users_id', + self::getTable() . '.consumed AS credit_consumed', + ], + 'FROM' => self::getTable(), + 'LEFT JOIN' => [ + 'glpi_tickets' => [ + 'ON' => [ + 'glpi_tickets' => 'id', + self::getTable() => 'tickets_id', + ], + ], + 'glpi_itilcategories' => [ + 'ON' => [ + 'glpi_itilcategories' => 'id', + 'glpi_tickets' => 'itilcategories_id', + ], + ], + ], + 'WHERE' => [ + self::getTable() . '.plugin_credit_entities_id' => $ID, + ], + 'ORDER' => [self::getTable() . '.id DESC'], + ]); + + $showuserlink = Session::haveRight('user', READ); + $usernames = []; + + foreach ($iterator as $data) { $Ticket = new Ticket(); - $Ticket->getFromDB($data['tickets_id']); + $ticket_link = ''; + $status = ''; + $type = ''; + // The ticket may have been purged since the consumption. + if ($data['id'] !== null) { + $Ticket->fields = $data; + $ticket_link = sprintf( + '%s', + htmlescape($Ticket->getLinkURL()), + htmlescape($Ticket->getNameID()), + ); + $status = Ticket::getStatus($data['status']); + $type = Ticket::getTicketTypeName($data['type']); + } - $itilcat = new ITILCategory(); $category = __('None'); - if ($itilcat->getFromDB($Ticket->fields['itilcategories_id'])) { - $category = $itilcat->getName(['comments' => true]); + if ($data['credit_category_name'] !== null) { + $itilcat = new ITILCategory(); + $itilcat->fields = [ + 'id' => $data['itilcategories_id'], + 'name' => $data['credit_category_name'], + 'completename' => $data['credit_category_completename'], + ]; + $category = $itilcat->getName(); } - $showuserlink = Session::haveRight('user', READ) ? 1 : 0; - - $ticket_url = $Ticket->getLinkURL(); - $ticket_name = $Ticket->getNameID(); - $username = $showuserlink !== 0 ? getUserLink($data['users_id']) : getUserName($data['users_id']); + $users_id = $data['credit_users_id']; + $usernames[$users_id] ??= $showuserlink ? getUserLink($users_id) : getUserName($users_id); $tickets_data[] = [ - 'ticket_link' => sprintf( - '%s', - htmlescape($ticket_url), - htmlescape($ticket_name), - ), - 'status' => Ticket::getStatus($Ticket->fields['status']), - 'type' => Ticket::getTicketTypeName($Ticket->fields['type']), + 'ticket_link' => $ticket_link, + 'status' => $status, + 'type' => $type, 'category' => $category, - 'date_creation' => $data["date_creation"], - 'username' => $username, - 'consumed' => $data['consumed'], + 'date_creation' => $data['credit_date_creation'], + 'username' => $usernames[$users_id], + 'consumed' => $data['credit_consumed'], ]; } } diff --git a/inc/ticketconfig.class.php b/inc/ticketconfig.class.php index 5662bca..3f4c002 100644 --- a/inc/ticketconfig.class.php +++ b/inc/ticketconfig.class.php @@ -161,6 +161,15 @@ public static function updateConfig(Ticket $ticket) } } + /** + * Remove the default voucher options of a purged ticket. + * Consumptions are kept: they still count against their voucher. + */ + public static function cleanForTicket(Ticket $ticket): void + { + (new self())->deleteByCriteria(['tickets_id' => $ticket->getID()]); + } + public static function install(Migration $migration) { /** @var DBmysql $DB */ diff --git a/setup.php b/setup.php index 77d2246..d7fc0f5 100644 --- a/setup.php +++ b/setup.php @@ -49,11 +49,14 @@ function plugin_init_credit() /** @var array $PLUGIN_HOOKS */ global $PLUGIN_HOOKS; - $plugin = new Plugin(); - $PLUGIN_HOOKS['csrf_compliant']['credit'] = true; - if (Session::getLoginUserID() && $plugin->isActivated('credit')) { + // Outside the session check: purges also run from cron and CLI. + $PLUGIN_HOOKS['item_purge']['credit'] = [ + 'Ticket' => ['PluginCreditTicketConfig', 'cleanForTicket'], + ]; + + if (Session::getLoginUserID()) { Plugin::registerClass( 'PluginCreditEntity', [