Skip to content
Snippets Groups Projects
Commit 6a06fb9b authored by Scott Euser's avatar Scott Euser
Browse files

Issue #3505245 by scott_euser: Drupal 11 support, test coverage

parent ace7bf76
No related branches found
No related tags found
1 merge request!6Resolve #3505245 "Drupal 11 support"
Pipeline #418322 passed
Showing
with 415 additions and 64 deletions
andrii
autoreference
autoreferences
categorising
chunker
collpsible
entityreference
euser
getbatch
nnevill
sakhaniuk
......@@ -2,7 +2,7 @@ name: AI Auto-reference
type: module
description: Automatically sets or suggests references to other content entities in the site.
package: AI Auto-reference
core_version_requirement: ^9 || ^10
core_version_requirement: ^10 || ^11
configure: ai_auto_reference.ai_auto_reference_settings_form
dependencies:
- drupal:ai
......@@ -5,10 +5,56 @@
* Update scripts for the AI Auto Reference module.
*/
use Drupal\node\Entity\NodeType;
/**
* Add auto_apply_relevance_levels to the default configuration for ai_auto_reference.settings.
* Add relevance levels to the default configuration settings.
*/
function ai_auto_reference_update_10001() {
$config = \Drupal::configFactory()->getEditable('ai_auto_reference.settings');
$config->set('auto_apply_relevance_levels', ['high', 'medium'])->save();
}
/**
* Convert to third party settings for config schema validation.
*/
function ai_auto_reference_update_10002() {
$bundles = NodeType::loadMultiple();
if (!$bundles) {
return;
}
foreach ($bundles as $bundle) {
$changed = FALSE;
$machine_name = $bundle->id();
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = \Drupal::entityTypeManager()
->getStorage('entity_form_display')
->load('node.' . $bundle->id() . '.default');
$components = $form_display_entity->getComponents();
foreach ($components as $field_name => $component) {
if (
is_array($component)
&& isset($component['third_party_settings'])
&& is_array($component['third_party_settings'])
&& !empty($component['third_party_settings']['ai_auto_reference'])
&& !empty($component['third_party_settings']['ai_auto_reference']['view_mode'])
) {
$changed = TRUE;
// Set third party setting instead.
$view_mode = $component['third_party_settings']['ai_auto_reference']['view_mode'];
$form_display_entity->setThirdPartySetting('ai_auto_reference', $field_name, [
'view_mode' => $view_mode,
]);
// Remove the original within the component.
unset($component['third_party_settings']['ai_auto_reference']);
$form_display_entity->setComponent($field_name, $component);
}
}
if ($changed) {
$form_display_entity->save();
}
}
}
......@@ -16,7 +16,8 @@ use Drupal\node\NodeInterface;
function ai_auto_reference_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$node = $form_state->getFormObject()->getEntity();
// Turning off the functionality on the 'create' page of nodes.
// The modules refreshes the page, which would just delete the progress on such pages.
// The modules refreshes the page, which would just delete the progress on
// such pages.
if (
$node instanceof NodeInterface
&& !$node->isNew()
......@@ -28,13 +29,15 @@ function ai_auto_reference_form_node_form_alter(&$form, FormStateInterface $form
// To prevent messages duplications.
if (!empty($apply_form['container'])) {
$apply_form_rendered = \Drupal::service('renderer')->renderRoot($apply_form);
\Drupal::messenger()->deleteByType('status');
\Drupal::messenger()->addMessage($apply_form);
\Drupal::messenger()->addMessage($apply_form_rendered);
}
$build_info = $form_state->getBuildInfo();
$node = $build_info['callback_object']->getEntity();
if ($node instanceof NodeInterface && $node->id()) {
$form['#attached']['library'][] = 'ai_auto_reference/ai-auto-reference-admin';
$form['actions']['ai_auto_reference'] = [
'#type' => 'submit',
'#value' => t('Generate references with AI'),
......
.fieldset__ai-auto-reference,
.fieldset__ai-auto-reference:not(.fieldgroup) {
.fieldset__ai-auto-reference:not(.fieldgroup),
.fieldset.fieldset__ai-auto-reference,
.fieldset.fieldset__ai-auto-reference:not(.fieldgroup) {
/** Prevent Gin theme compatibility issue for the apply form */
background-color: white;
}
{
"name": "drupal/ai_auto_reference",
"description": "Automatically sets or suggests references to other content entities in the site.",
"keywords": ["openai", "GPT-3", "GPT-4", "content generation", "AI content", "gpt", "content moderation"],
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Scott Euser",
"homepage": "https://scotteuser.com",
"role": "Maintainer"
},
{
"name": "Andrii Sakhaniuk",
"homepage": "https://dev-branch.com",
"role": "Maintainer"
}
]
"name": "drupal/ai_auto_reference",
"description": "Automatically sets or suggests references to other content entities in the site.",
"keywords": ["openai", "GPT-3", "GPT-4", "content generation", "AI content", "gpt", "content moderation"],
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"require": {
"drupal/ai": "^1.0.4"
},
"authors": [
{
"name": "Scott Euser",
"homepage": "https://scotteuser.com",
"role": "Maintainer"
},
{
"name": "Andrii Sakhaniuk",
"homepage": "https://dev-branch.com",
"role": "Maintainer"
}
]
}
service: "gpt"
service_model: "gpt-3.5-turbo"
service_api_key: ""
token_limit: "4000"
token_limit: 4000
auto_apply_suggestions: false
auto_apply_relevance_levels: ["high","medium"]
core.entity_view_display.*.*.*.third_party.ai_auto_reference:
type: sequence
label: 'AI Auto-Reference field settings'
sequence:
type: mapping
label: An AI Auto-Reference
mapping:
view_mode:
type: string
label: 'The view mode'
core.entity_form_display.*.*.*.third_party.ai_auto_reference:
type: sequence
label: 'AI Auto-Reference field settings'
sequence:
type: mapping
label: An AI Auto-Reference
mapping:
view_mode:
type: string
label: 'The view mode'
......@@ -19,4 +19,6 @@ ai_auto_reference.settings:
type: boolean
auto_apply_relevance_levels:
label: 'Automatically save the suggestions at the specified relevance without editor review'
type: array
\ No newline at end of file
type: sequence
sequence:
type: string
......@@ -5,8 +5,6 @@ namespace Drupal\ai_auto_reference;
use Drupal\ai\AiProviderPluginManager;
use Drupal\ai\OperationType\Chat\ChatInput;
use Drupal\ai\OperationType\Chat\ChatMessage;
use Drupal\ai\OperationType\GenericType\AudioFile;
use Drupal\ai\OperationType\SpeechToText\SpeechToTextInput;
use Drupal\ai\Utility\TextChunkerInterface;
use Drupal\ai\Utility\TokenizerInterface;
use Drupal\Component\Serialization\Json;
......@@ -56,7 +54,7 @@ class AiReferenceGenerator {
* The AI provider plugin manager.
* @param \Drupal\ai\Utility\TokenizerInterface $tokenizer
* The tokenizer.
* @param \Drupal\ai\Utility\TextChunkerInterface
* @param \Drupal\ai\Utility\TextChunkerInterface $textChunker
* The text chunker.
*/
public function __construct(
......@@ -84,17 +82,22 @@ class AiReferenceGenerator {
* Array with configurations.
*/
public function getBundleAiReferencesConfiguration($bundle) {
$form_display = $this->entityTypeManager
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = $this->entityTypeManager
->getStorage('entity_form_display')
->load("node.{$bundle}.default");
$settings = $form_display_entity->getThirdPartySettings('ai_auto_reference');
if (empty($settings)) {
return $settings;
}
$ai_references = [];
foreach ($form_display->getComponents() as $field_name => $component) {
foreach ($settings as $field_name => $setting) {
// Add AI autogenerate button only if at least one field is configured.
if (!empty(($component['third_party_settings']['ai_auto_reference']))) {
if (!empty($setting['view_mode'])) {
$ai_references[] = [
'field_name' => $field_name,
'view_mode' => $component['third_party_settings']['ai_auto_reference']['view_mode'],
'view_mode' => $setting['view_mode'],
];
}
}
......
......@@ -42,7 +42,7 @@ class AiReferenceBatch {
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
RendererInterface $renderer,
ConfigFactoryInterface $config
ConfigFactoryInterface $config,
) {
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
......
......@@ -36,7 +36,7 @@ class AutoReferenceApplyForm extends FormBase {
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
RequestStack $request_stack
RequestStack $request_stack,
) {
$this->entityTypeManager = $entity_type_manager;
$this->requestStack = $request_stack;
......@@ -74,7 +74,7 @@ class AutoReferenceApplyForm extends FormBase {
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, NodeInterface $node = NULL, array $configuration = NULL) {
public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL, ?array $configuration = NULL) {
$form = [];
$query = $this->requestStack->getCurrentRequest()->query->all();
$auto_apply = !empty($query['auto-apply']);
......
......@@ -96,17 +96,18 @@ class AutoReferenceDeleteForm extends ConfirmFormBase {
if (empty($this->nodeType) || empty($this->fieldName)) {
return;
}
$entity = $this->entityTypeManager
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = $this->entityTypeManager
->getStorage('entity_form_display')
->load("node.{$this->nodeType}.default");
->load('node.' . $this->nodeType . '.default');
if (!$entity) {
if (!$form_display_entity) {
return;
}
$field = $entity->getComponent($this->fieldName);
unset($field['third_party_settings']['ai_auto_reference']);
$entity->setComponent($this->fieldName, $field);
$entity->save();
$form_display_entity->unsetThirdPartySetting('ai_auto_reference', $this->fieldName);
$form_display_entity->save();
$form_state->setRedirectUrl($this->getCancelUrl());
}
......
......@@ -57,7 +57,7 @@ class AutoReferenceEditForm extends FormBase {
public function __construct(
EntityDisplayRepositoryInterface $entity_display_repository,
EntityTypeManagerInterface $entity_type_manager,
EntityFieldManagerInterface $entity_field_manager
EntityFieldManagerInterface $entity_field_manager,
) {
$this->entityDisplayRepository = $entity_display_repository;
$this->entityTypeManager = $entity_type_manager;
......@@ -91,6 +91,7 @@ class AutoReferenceEditForm extends FormBase {
return $form;
}
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = $this->entityTypeManager
->getStorage('entity_form_display')
->load("node.{$node_type}.default");
......@@ -99,9 +100,8 @@ class AutoReferenceEditForm extends FormBase {
return $form;
}
$field = $form_display_entity->getComponent($field_name);
if (empty($field['third_party_settings']['ai_auto_reference']['view_mode'])) {
$setting = $form_display_entity->getThirdPartySetting('ai_auto_reference', $field_name);
if (!is_array($setting) || empty($setting['view_mode'])) {
return $form;
}
......@@ -112,7 +112,7 @@ class AutoReferenceEditForm extends FormBase {
'#title' => $this->t('View mode'),
'#description' => $this->t('View mode that will be used as a source of content for AI analysis.'),
'#options' => $view_modes,
'#default_value' => $field['third_party_settings']['ai_auto_reference']['view_mode'],
'#default_value' => $setting['view_mode'],
'#required' => TRUE,
];
$form['bundle'] = [
......@@ -141,18 +141,18 @@ class AutoReferenceEditForm extends FormBase {
$bundle = $values['bundle'];
$field_name = $values['field_name'];
/** @var \Drupal\Core\Entity\Entity\EntityFormDisplay $entity */
$entity = $this->entityTypeManager
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = $this->entityTypeManager
->getStorage('entity_form_display')
->load("{$this->entityType}.{$bundle}.default");
if (!$entity) {
if (!$form_display_entity) {
return;
}
$field = $entity->getComponent($field_name);
$field['third_party_settings']['ai_auto_reference']['view_mode'] = $values['view_mode'];
$entity->setComponent($field_name, $field);
$entity->save();
$form_display_entity->setThirdPartySetting('ai_auto_reference', $field_name, [
'view_mode' => $values['view_mode'],
]);
$form_display_entity->save();
$form_state->setRedirect($this->redirectPath, ['node_type' => $bundle]);
}
......
......@@ -81,7 +81,7 @@ class EntityBundleSettingsForm extends FormBase {
public function __construct(
EntityFieldManagerInterface $entity_field_manager,
EntityTypeManagerInterface $entity_type_manager,
EntityDisplayRepositoryInterface $entity_display_repository
EntityDisplayRepositoryInterface $entity_display_repository,
) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
......@@ -139,11 +139,9 @@ class EntityBundleSettingsForm extends FormBase {
}
$field_name = $table['add_new_autoreference']['field_name'];
$field = $entity_form_display->getComponent($field_name);
$field['third_party_settings']['ai_auto_reference'] = [
$entity_form_display->setThirdPartySetting('ai_auto_reference', $field_name, [
'view_mode' => $table['add_new_autoreference']['view_mode'],
];
$entity_form_display->setComponent($field_name, $field);
]);
$entity_form_display->save();
}
......@@ -185,6 +183,7 @@ class EntityBundleSettingsForm extends FormBase {
}
}
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */
$form_display_entity = $this->entityTypeManager
->getStorage('entity_form_display')
->load("$this->entityType.$this->bundleName.default");
......@@ -196,9 +195,9 @@ class EntityBundleSettingsForm extends FormBase {
$view_modes = $this->entityDisplayRepository->getViewModeOptionsByBundle($this->entityType, $this->bundleName);
// Displaying already added AI autoreference blocks.
$settings = $form_display_entity->getThirdPartySettings('ai_auto_reference');
foreach ($reference_fields as $field_name => $label) {
$field = $form_display_entity->getComponent($field_name);
if (empty($field['third_party_settings']['ai_auto_reference']['view_mode'])) {
if (empty($settings[$field_name]['view_mode'])) {
continue;
}
$parameters = [
......@@ -207,7 +206,7 @@ class EntityBundleSettingsForm extends FormBase {
];
$table[] = [
'field_name' => ['#markup' => "{$label} ({$field_name})"],
'view_mode' => ['#markup' => $view_modes[$field['third_party_settings']['ai_auto_reference']['view_mode']]],
'view_mode' => ['#markup' => $view_modes[$settings[$field_name]['view_mode']]],
'actions' => [
'#type' => 'operations',
'#links' => [
......
......@@ -21,14 +21,14 @@ class SettingsForm extends ConfigFormBase {
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Config\TypedConfigManagerInterface|null $typedConfigManager
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* The typed config manager.
* @param \Drupal\ai\AiProviderPluginManager $aiProviderManager
* The AI provider manager.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
protected $typedConfigManager,
protected TypedConfigManagerInterface $typedConfigManager,
protected AiProviderPluginManager $aiProviderManager,
) {
parent::__construct($config_factory, $typedConfigManager);
......@@ -135,7 +135,7 @@ class SettingsForm extends ConfigFormBase {
$relevance_levels = array_filter($form_state->getValue('auto_apply_relevance_levels'));
$config = $this->config('ai_auto_reference.settings')
->set('provider', $form_state->getValue('provider'))
->set('token_limit', $form_state->getValue('token_limit'))
->set('token_limit', (int) $form_state->getValue('token_limit'))
->set('auto_apply_suggestions', $form_state->getValue('auto_apply_suggestions'))
->set('auto_apply_relevance_levels', array_keys($relevance_levels));
$config->save();
......
<?php
namespace Drupal\Tests\ai_auto_reference\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Contains AI Auto-Reference UI setup functional tests.
*
* @group ai_auto_reference
*/
class AiAutoReferenceUiTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'ai',
'ai_auto_reference',
'node',
'taxonomy',
'user',
'system',
'field_ui',
'views_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Nodes for testing the indexing.
*
* @var array
* An array of nodes for testing.
*/
protected $nodes = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'administer content types',
'access content overview',
'administer nodes',
'administer node fields',
'bypass node access',
'administer ai',
'administer ai autoreference',
]);
// Create an entity reference field "Related food" for articles.
$this->createEntityReferenceField(
'node',
'article',
'field_related_food',
'Related food',
'node',
['article'],
);
$this->createSampleContent();
}
/**
* Helper function to create an entity reference field.
*
* @param string $entity_type
* The entity type (e.g., 'node').
* @param string $bundle
* The bundle (content type).
* @param string $field_name
* The machine name of the field.
* @param string $field_label
* The human-readable label.
* @param string $target_type
* The entity type being referenced.
* @param array $target_bundles
* Allowed target bundles.
*/
protected function createEntityReferenceField($entity_type, $bundle, $field_name, $field_label, $target_type, array $target_bundles) {
$entity_type_manager = \Drupal::entityTypeManager();
$display_repository = \Drupal::service('entity_display.repository');
// Create the field storage.
$field_storage = $entity_type_manager->getStorage('field_storage_config')->create([
'field_name' => $field_name,
'entity_type' => $entity_type,
'type' => 'entity_reference',
'settings' => [
'target_type' => $target_type,
],
'cardinality' => -1,
'translatable' => FALSE,
]);
$field_storage->save();
// Create the field configuration.
$field = $entity_type_manager->getStorage('field_config')->create([
'field_name' => $field_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'label' => $field_label,
'settings' => [
'handler' => 'default',
'handler_settings' => [
'target_bundles' => array_combine($target_bundles, $target_bundles),
],
],
]);
$field->save();
// Set form display.
$form_display = $display_repository->getFormDisplay($entity_type, $bundle, 'default');
$form_display->setComponent($field_name, [
'type' => 'entity_reference_autocomplete',
])->save();
// Set view display.
$view_display = $display_repository->getViewDisplay($entity_type, $bundle, 'default');
$view_display->setComponent($field_name, [
'type' => 'entity_reference_label',
])->save();
}
/**
* Create sample content to check index.
*/
public function createSampleContent(): void {
$this->nodes[] = $this->drupalCreateNode([
'type' => 'article',
'title' => 'Chocolate Cake',
'field_body' => [
'value' => 'A delicious chocolate dessert made with cocoa powder and dark chocolate.',
'format' => 'plain_text',
],
]);
$this->nodes[] = $this->drupalCreateNode([
'type' => 'article',
'title' => 'Strawberry Cheese Cake',
'field_body' => [
'value' => 'A sweet cheese based dessert make with strawberries on a pie-like crust.',
'format' => 'plain_text',
],
'status' => 0,
]);
$this->nodes[] = $this->drupalCreateNode([
'type' => 'article',
'title' => 'Vanilla Ice Cream',
'field_body' => [
'value' => 'A creamy vanilla dessert made with milk, cream, and vanilla extract.',
'format' => 'plain_text',
],
]);
$this->nodes[] = $this->drupalCreateNode([
'type' => 'article',
'title' => 'Tomato Soup',
'field_body' => [
'value' => 'A warm starter made with fresh tomatoes, garlic, and basil.',
'format' => 'plain_text',
],
]);
}
/**
* Test the access.
*/
public function testSettingsPageAccess() {
$this->drupalGet('admin/config/content/ai-autoreference-settings');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/structure/types/manage/article/ai-autoreference');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Test the settings.
*/
public function testNodeSettings() {
$this->drupalLogin($this->adminUser);
// Check that no button exists yet.
$this->drupalGet('node/1/edit');
$this->assertSession()->buttonNotExists('Generate references with AI');
// Create a new auto-reference.
$this->drupalGet('admin/structure/types/manage/article/ai-autoreference');
$this->submitForm([
'table[add_new_autoreference][field_name]' => 'field_related_food',
'table[add_new_autoreference][view_mode]' => 'teaser',
], 'Add autoreference');
// Now edit it.
$this->clickLink('Edit');
// Re-save.
$this->submitForm([], 'Save settings');
// Delete it.
$this->drupalGet('admin/structure/types/manage/article/ai-autoreference/field_related_food/delete');
$this->submitForm([], 'Confirm');
// Add it again.
$this->submitForm([
'table[add_new_autoreference][field_name]' => 'field_related_food',
'table[add_new_autoreference][view_mode]' => 'teaser',
], 'Add autoreference');
// Edit a node.
$this->drupalGet('node/1/edit');
$this->assertSession()->buttonExists('Generate references with AI');
// Pretend we have generated references.
$url = Url::fromRoute('entity.node.edit_form', ['node' => 1], [
'query' => [
'field_related_food[h]' => '1,2',
'field_related_food[m]' => '3',
'auto-apply' => '',
],
]);
$this->drupalGet($url);
// High results.
$this->assertSession()->elementTextContains(
'css',
'.js-form-item-container-field-related-food-h-1 label',
'Chocolate Cake',
);
$this->assertSession()->elementTextContains(
'css',
'.js-form-item-container-field-related-food-h-2 label',
'Strawberry Cheese Cake',
);
// Medium results.
$this->assertSession()->elementTextContains(
'css',
'.js-form-item-container-field-related-food-m-3 label',
'Vanilla Ice Cream',
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment