Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Fix infinite loop / Gateway Timeout when a ticket's assigned technician and assigned group are changed simultaneously, caused by a synchronous actor removal during `pre_item_update()`
- Fixed group reassignment to remove previously assigned groups only when using the Escalade reassignment action
- Fixed `_plugin_escalade_rules_only` being ignored on group assignments, so callers could not opt out of escalade's automatic processing

## [2.10.6] - 2026-07-31

Expand Down
1 change: 1 addition & 0 deletions hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ function plugin_escalade_pre_item_add_group_ticket($item)
if (
$item instanceof Group_Ticket
&& $item->input['type'] == CommonITILActor::ASSIGN
&& empty($item->input['_plugin_escalade_rules_only'])
) {
if (!isset($_SESSION['plugin_escalade']['current_group_assignment'])) {
$_SESSION['plugin_escalade']['current_group_assignment'] = [];
Expand Down
8 changes: 8 additions & 0 deletions inc/ticket.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,14 @@ public static function addHistoryOnAddGroup(CommonDBTM $item)

public static function processAfterAddGroup(Group_Ticket $item)
{
// Explicit opt-out for callers that assign a technician group on their own
// (other plugins, scripts). Same meaning as in pre_item_update(): escalade
// skips its logic entirely, so no group cleanup, no technician unassignment,
// no history entry and no automatic status change for this assignment.
if (!empty($item->input['_plugin_escalade_rules_only'])) {
return;
}

$tickets_id = $item->fields['tickets_id'];
$groups_id = $item->fields['groups_id'];

Expand Down
101 changes: 101 additions & 0 deletions tests/Units/GroupEscalationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,107 @@ public function testStandardGroupAssignmentKeepsExistingGroups(): void
]));
}

/**
* Without the opt-out flag, adding a technician group triggers the escalade
* processing: the previously assigned technician is unassigned.
*/
public function testGroupAssignmentWithoutOptOutRemovesTechnician(): void
{
$this->initConfig([
'remove_tech' => 1,
'show_history' => 1,
]);

$tech = getItemByTypeName(User::class, 'tech');
$group = $this->createGroup('no_opt_out_group_' . uniqid());

$ticket = $this->createItem(Ticket::class, [
'name' => 'Group assignment without opt-out',
'content' => '',
'_actors' => [
'assign' => [
[
'items_id' => $tech->getID(),
'itemtype' => 'User',
],
],
],
]);

$group_ticket = new Group_Ticket();
$this->assertNotFalse($group_ticket->add([
'tickets_id' => $ticket->getID(),
'groups_id' => $group->getID(),
'type' => CommonITILActor::ASSIGN,
]));

$this->assertEquals(0, countElementsInTable(Ticket_User::getTable(), [
'tickets_id' => $ticket->getID(),
'users_id' => $tech->getID(),
'type' => CommonITILActor::ASSIGN,
]));

$this->assertEquals(1, countElementsInTable('glpi_plugin_escalade_histories', [
'tickets_id' => $ticket->getID(),
]));
}

/**
* A caller that assigns a technician group on its own can opt out of the
* escalade processing with _plugin_escalade_rules_only: the technician stays
* assigned and no escalation history entry is created.
*/
public function testGroupAssignmentWithOptOutKeepsTechnician(): void
{
$this->initConfig([
'remove_tech' => 1,
'show_history' => 1,
]);

$tech = getItemByTypeName(User::class, 'tech');
$group = $this->createGroup('opt_out_group_' . uniqid());

$ticket = $this->createItem(Ticket::class, [
'name' => 'Group assignment with opt-out',
'content' => '',
'_actors' => [
'assign' => [
[
'items_id' => $tech->getID(),
'itemtype' => 'User',
],
],
],
]);

$group_ticket = new Group_Ticket();
$this->assertNotFalse($group_ticket->add([
'tickets_id' => $ticket->getID(),
'groups_id' => $group->getID(),
'type' => CommonITILActor::ASSIGN,
'_plugin_escalade_rules_only' => true,
]));

// The group is still linked to the ticket...
$this->assertEquals(1, countElementsInTable(Group_Ticket::getTable(), [
'tickets_id' => $ticket->getID(),
'groups_id' => $group->getID(),
'type' => CommonITILActor::ASSIGN,
]));

// ... but escalade did not unassign the technician.
$this->assertEquals(1, countElementsInTable(Ticket_User::getTable(), [
'tickets_id' => $ticket->getID(),
'users_id' => $tech->getID(),
'type' => CommonITILActor::ASSIGN,
]));

// ... and did not record an escalation.
$this->assertEquals(0, countElementsInTable('glpi_plugin_escalade_histories', [
'tickets_id' => $ticket->getID(),
]));
}

/**
* A real Escalade reassignment must remove old groups while preserving
* every previously assigned group in the visual assignment history.
Expand Down