From c60c2d94eb4e0c503d6a38590b2d670d841600b0 Mon Sep 17 00:00:00 2001 From: Lainow Date: Tue, 25 Aug 2026 09:45:33 +0200 Subject: [PATCH 1/3] Improve compatibility with the Escalade plugin --- src/Config.php | 86 ++++++++++++++++++++++++++++++++++++++ src/Controller.php | 19 ++++++++- templates/config.html.twig | 12 +++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/src/Config.php b/src/Config.php index 922ff94..07f3e82 100644 --- a/src/Config.php +++ b/src/Config.php @@ -41,6 +41,7 @@ use Entity; use Glpi\Application\View\TemplateRenderer; use Migration; +use Plugin; use Session; class Config extends CommonDBTM @@ -192,6 +193,90 @@ public static function getSelectableActorGroup(): array ]; } + /** + * Escalade provides the same feature ("Use the technician's group") with a + * global configuration, while this plugin configures it per entity. When the + * Escalade option is effectively enabled, it takes precedence over ours. + */ + public static function isTechnicianGroupHandledByEscalade(): bool + { + if (!Plugin::isPluginActive('escalade')) { + return false; + } + + $escalade_config = self::getEscaladeConfig(); + if ($escalade_config === null) { + return false; + } + + // The main option only selects which groups are used: the feature stays + // inert unless it is also enabled on creation and/or on modification. + if ((int) ($escalade_config['use_assign_user_group'] ?? 0) === 0) { + return false; + } + + // Older Escalade versions have no sub-options: the main one was enough. + $on_creation = (int) ($escalade_config['use_assign_user_group_creation'] ?? 1) !== 0; + $on_modification = (int) ($escalade_config['use_assign_user_group_modification'] ?? 1) !== 0; + + // On creation, Escalade steps aside when the Behaviors plugin owns the + // feature (see PluginEscaladeTicket::assignUserGroup()). + if ($on_creation && self::isTechnicianGroupHandledByBehaviors()) { + $on_creation = false; + } + + return $on_creation || $on_modification; + } + + /** + * Get the Escalade configuration, from the session when available, from the + * database otherwise (CLI, tests, ...). + * + * @return array|null + */ + private static function getEscaladeConfig(): ?array + { + /** @var \DBmysql $DB */ + global $DB; + + if (isset($_SESSION['glpi_plugins']['escalade']['config']) && is_array($_SESSION['glpi_plugins']['escalade']['config'])) { + return $_SESSION['glpi_plugins']['escalade']['config']; + } + + $table = 'glpi_plugin_escalade_configs'; + if (!$DB->tableExists($table) || !$DB->fieldExists($table, 'use_assign_user_group')) { + return null; + } + + $fields = ['use_assign_user_group']; + foreach (['use_assign_user_group_creation', 'use_assign_user_group_modification'] as $field) { + if ($DB->fieldExists($table, $field)) { + $fields[] = $field; + } + } + + $escalade_config = $DB->request([ + 'SELECT' => $fields, + 'FROM' => $table, + 'LIMIT' => 1, + ])->current(); + + return is_array($escalade_config) ? $escalade_config : null; + } + + /** + * The Behaviors plugin provides the same feature too, and Escalade gives it + * precedence on ticket creation. + */ + private static function isTechnicianGroupHandledByBehaviors(): bool + { + if (!Plugin::isPluginActive('behaviors') || !class_exists(\GlpiPlugin\Behaviors\Config::class)) { + return false; + } + + return (int) \GlpiPlugin\Behaviors\Config::getInstance()->getField('use_assign_user_group') !== 0; + } + public static function showForEntity(Entity $item): void { $moconfig = new self(); @@ -217,6 +302,7 @@ public static function showForEntity(Entity $item): void 'dropdown_options' => self::getSelectableActorGroup(), 'inheritance_labels' => $inheritance_labels, 'config_parent' => self::CONFIG_PARENT, + 'escalade_takes_technician_group' => self::isTechnicianGroupHandledByEscalade(), 'params' => [ 'canedit' => self::canUpdate(), ], diff --git a/src/Controller.php b/src/Controller.php index 19b2f8d..70ad728 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -94,6 +94,12 @@ public static function useConfig(CommonDBTM $item): void } } elseif ($item->fields['type'] == \CommonITILActor::ASSIGN) { if ($moconfig->fields['take_technician_group_ticket'] != 0) { + if (Config::isTechnicianGroupHandledByEscalade()) { + // Escalade handles the same feature with a global + // configuration: it takes precedence over ours, so we + // just skip our own processing. + return; + } self::addGroupsForActorType($item, $moconfig, \CommonITILActor::ASSIGN, 'take_technician_group_ticket', 'Ticket'); } } @@ -209,6 +215,15 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig $object->getFromDB($item->fields[$idField]); + // Escalade reacts to every technician group assignment: it would keep only + // the last group added and unassign the technicians. This flag asks it to + // skip its own processing for the assignments we make here, which also means + // no group cleanup, no escalation history entry and no automatic status + // change on its side. + $escalade_options = $groupClass === Group_Ticket::class + ? ['_plugin_escalade_rules_only' => true] + : []; + $actors = $object->getActorsForType($actorType); foreach ($actors as $actor) { if (!is_array($actor) || !isset($actor['itemtype']) || $actor['itemtype'] !== 'User') { @@ -229,7 +244,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig ]; if (!$t_group->getFromDBByCrit($criteria)) { - $t_group->add($criteria); + $t_group->add($criteria + $escalade_options); } } else { // Use all groups of the user @@ -250,7 +265,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig ]; if (!$t_group->getFromDBByCrit($criteria)) { - $t_group->add($criteria); + $t_group->add($criteria + $escalade_options); } } } diff --git a/templates/config.html.twig b/templates/config.html.twig index c3ddee3..302387a 100644 --- a/templates/config.html.twig +++ b/templates/config.html.twig @@ -41,6 +41,16 @@ {% set dropdown_options_with_inherit = inherit_option + dropdown_options %} {% set cell_options = {'no_label': true, 'field_class': 'col-12'} %} +{# Escalade owns the same feature with a global configuration: our per-entity option is then inoperative #} +{% set escalade_takes_technician_group = escalade_takes_technician_group|default(false) %} +{% set escalade_technician_group_warning %} + {% if escalade_takes_technician_group %} +
+ {{ __('The "Use the technician\'s group" option from Escalade is enabled and takes precedence over this one.', 'moreoptions') }} +
+ {% endif %} +{% endset %} +
{% set item_has_pictures = item.hasItemtypeOrModelPictures() %} @@ -81,7 +91,7 @@ {{ __('Take the technician group', 'moreoptions') }} - {{ fields.dropdownArrayField('take_technician_group_ticket', item.fields['take_technician_group_ticket'], dropdown_options_with_inherit, '', cell_options|merge({'add_field_html': inheritance_labels['take_technician_group_ticket']|default(null)})) }} + {{ fields.dropdownArrayField('take_technician_group_ticket', item.fields['take_technician_group_ticket'], dropdown_options_with_inherit, '', cell_options|merge({'disabled': escalade_takes_technician_group, 'add_field_html': (inheritance_labels['take_technician_group_ticket']|default('')) ~ escalade_technician_group_warning})) }} {{ fields.dropdownArrayField('take_technician_group_change', item.fields['take_technician_group_change'], dropdown_options_with_inherit, '', cell_options|merge({'add_field_html': inheritance_labels['take_technician_group_change']|default(null)})) }} {{ fields.dropdownArrayField('take_technician_group_problem', item.fields['take_technician_group_problem'], dropdown_options_with_inherit, '', cell_options|merge({'add_field_html': inheritance_labels['take_technician_group_problem']|default(null)})) }} From 6de6d7632613a49dd908d4198ee3563743987e91 Mon Sep 17 00:00:00 2001 From: Lainow Date: Tue, 25 Aug 2026 11:26:46 +0200 Subject: [PATCH 2/3] Update changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 791fb1d..52eb2d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,5 +6,3 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] - - From e9669e003dd5b8e53372dabd331d3a1a8e7fd96e Mon Sep 17 00:00:00 2001 From: Lainow Date: Tue, 25 Aug 2026 15:55:39 +0200 Subject: [PATCH 3/3] implements suggestions --- src/Config.php | 23 +++++++++++- tests/Units/ConfigTest.php | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/Config.php b/src/Config.php index 07f3e82..00c53e1 100644 --- a/src/Config.php +++ b/src/Config.php @@ -204,7 +204,26 @@ public static function isTechnicianGroupHandledByEscalade(): bool return false; } - $escalade_config = self::getEscaladeConfig(); + return self::escaladeConfigHandlesTechnicianGroup( + self::getEscaladeConfig(), + self::isTechnicianGroupHandledByBehaviors(), + ); + } + + /** + * Tell whether an Escalade configuration effectively handles the technician + * group assignment. + * + * Holds the decision alone, without reading the plugins state, so that it can + * be tested without having Escalade nor Behaviors installed. + * + * @param array|null $escalade_config Escalade configuration, `null` when it cannot be read + * @param bool $handled_by_behaviors Whether the Behaviors plugin owns the feature + */ + public static function escaladeConfigHandlesTechnicianGroup( + ?array $escalade_config, + bool $handled_by_behaviors = false, + ): bool { if ($escalade_config === null) { return false; } @@ -221,7 +240,7 @@ public static function isTechnicianGroupHandledByEscalade(): bool // On creation, Escalade steps aside when the Behaviors plugin owns the // feature (see PluginEscaladeTicket::assignUserGroup()). - if ($on_creation && self::isTechnicianGroupHandledByBehaviors()) { + if ($on_creation && $handled_by_behaviors) { $on_creation = false; } diff --git a/tests/Units/ConfigTest.php b/tests/Units/ConfigTest.php index 31954a7..a0d4d1d 100644 --- a/tests/Units/ConfigTest.php +++ b/tests/Units/ConfigTest.php @@ -38,6 +38,7 @@ use GlpiPlugin\Moreoptions\Config; use GlpiPlugin\Moreoptions\Tests\MoreOptionsTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class ConfigTest extends MoreOptionsTestCase { @@ -2413,4 +2414,80 @@ public function testMixedFieldInheritanceThroughThreeLevels(): void 'Child should keep its own explicit value (0), ignoring parent and grandparent', ); } + + /** + * @return iterable|null, bool, bool}> + */ + public static function escaladeTechnicianGroupProvider(): iterable + { + yield 'no readable configuration' => [null, false, false]; + yield 'main option disabled' => [ + ['use_assign_user_group' => 0, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 1], + false, + false, + ]; + yield 'main option enabled but inert' => [ + ['use_assign_user_group' => 1, 'use_assign_user_group_creation' => 0, 'use_assign_user_group_modification' => 0], + false, + false, + ]; + yield 'enabled on creation only' => [ + ['use_assign_user_group' => 1, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 0], + false, + true, + ]; + yield 'enabled on modification only' => [ + ['use_assign_user_group' => 2, 'use_assign_user_group_creation' => 0, 'use_assign_user_group_modification' => 1], + false, + true, + ]; + yield 'enabled on both' => [ + ['use_assign_user_group' => 2, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 1], + false, + true, + ]; + yield 'values stored as strings' => [ + ['use_assign_user_group' => '1', 'use_assign_user_group_creation' => '1', 'use_assign_user_group_modification' => '0'], + false, + true, + ]; + yield 'older version without sub options' => [ + ['use_assign_user_group' => 1], + false, + true, + ]; + yield 'unrelated configuration' => [['remove_tech' => 1], false, false]; + yield 'behaviors owns the creation, nothing left' => [ + ['use_assign_user_group' => 1, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 0], + true, + false, + ]; + yield 'behaviors owns the creation, modification remains' => [ + ['use_assign_user_group' => 1, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 1], + true, + true, + ]; + yield 'behaviors is irrelevant when the main option is off' => [ + ['use_assign_user_group' => 0, 'use_assign_user_group_creation' => 1, 'use_assign_user_group_modification' => 1], + true, + false, + ]; + } + + /** + * Test the decision on its own, without requiring Escalade to be installed + * + * @param array|null $escalade_config + */ + #[DataProvider('escaladeTechnicianGroupProvider')] + public function testEscaladeConfigHandlesTechnicianGroup( + ?array $escalade_config, + bool $handled_by_behaviors, + bool $expected, + ): void { + $this->assertSame( + $expected, + Config::escaladeConfigHandlesTechnicianGroup($escalade_config, $handled_by_behaviors), + ); + } }