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
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]


105 changes: 105 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Entity;
use Glpi\Application\View\TemplateRenderer;
use Migration;
use Plugin;
use Session;

class Config extends CommonDBTM
Expand Down Expand Up @@ -192,6 +193,109 @@ 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
Comment thread
Lainow marked this conversation as resolved.
{
if (!Plugin::isPluginActive('escalade')) {
return false;
}

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<string, mixed>|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;
}

// 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 && $handled_by_behaviors) {
$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<string, mixed>|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();
Expand All @@ -217,6 +321,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(),
],
Expand Down
19 changes: 17 additions & 2 deletions src/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand Down Expand Up @@ -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') {
Expand All @@ -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
Expand All @@ -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);
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion templates/config.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
<div class="text-warning small mt-1">
<i class="ti ti-alert-triangle me-1"></i>{{ __('The "Use the technician\'s group" option from Escalade is enabled and takes precedence over this one.', 'moreoptions') }}
</div>
{% endif %}
{% endset %}

<div class="asset">
<form action="{{ path('plugins/moreoptions/front/config.form.php') }}" method="post">
{% set item_has_pictures = item.hasItemtypeOrModelPictures() %}
Expand Down Expand Up @@ -81,7 +91,7 @@
</tr>
<tr>
<td>{{ __('Take the technician group', 'moreoptions') }}</td>
<td>{{ 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)})) }}</td>
<td>{{ 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})) }}</td>
<td>{{ 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)})) }}</td>
<td>{{ 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)})) }}</td>
</tr>
Expand Down
77 changes: 77 additions & 0 deletions tests/Units/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

use GlpiPlugin\Moreoptions\Config;
use GlpiPlugin\Moreoptions\Tests\MoreOptionsTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class ConfigTest extends MoreOptionsTestCase
{
Expand Down Expand Up @@ -2413,4 +2414,80 @@ public function testMixedFieldInheritanceThroughThreeLevels(): void
'Child should keep its own explicit value (0), ignoring parent and grandparent',
);
}

/**
* @return iterable<string, array{array<string, mixed>|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<string, mixed>|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),
);
}
}