diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml
new file mode 100644
index 0000000..afc1ddf
--- /dev/null
+++ b/.github/workflows/review.yml
@@ -0,0 +1,27 @@
+name: Review
+
+on: [pull_request]
+
+env:
+ TARGET_DRUPAL_CORE_VERSION: 11
+
+jobs:
+ drupal-lint:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Lint Drupal
+ run: |
+ docker compose --profile lint run drupal-lint
+
+ drupal-check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check Drupal compatibility
+ run: |
+ docker compose --profile lint run drupal-check
diff --git a/README.md b/README.md
index 7241327..4f4e665 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# AI Sorting
-Intelligent content ordering for Drupal Views using machine learning. Content automatically learns which items engage users most and surfaces the best-performing content.
+Intelligent content ordering for Drupal Views using machine learning. Content
+automatically learns which items engage users most and surfaces the
+best-performing content.
## Features
@@ -36,7 +38,8 @@ Intelligent content ordering for Drupal Views using machine learning. Content au
## Configuration
- **Cache Lifetime** - How often content order refreshes
-- **Automatic Cache Setup** - Views cache automatically configured for optimal performance
+- **Automatic Cache Setup** - Views cache automatically configured for optimal
+ performance
## Dependencies
@@ -44,4 +47,5 @@ Intelligent content ordering for Drupal Views using machine learning. Content au
## Related Modules
-- [RL module](https://www.drupal.org/project/rl) - Core Thompson Sampling algorithm and API for developers
\ No newline at end of file
+- [RL module](https://www.drupal.org/project/rl) - Core Thompson Sampling
+ algorithm and API for developers
diff --git a/ai_sorting.info.yml b/ai_sorting.info.yml
index 1da7996..5711118 100644
--- a/ai_sorting.info.yml
+++ b/ai_sorting.info.yml
@@ -5,4 +5,4 @@ core_version_requirement: ^10.3 | ^11
package: Custom
dependencies:
- drupal:views
- - rl
\ No newline at end of file
+ - rl
diff --git a/ai_sorting.libraries.yml b/ai_sorting.libraries.yml
index 88ecedb..bad7f68 100644
--- a/ai_sorting.libraries.yml
+++ b/ai_sorting.libraries.yml
@@ -12,4 +12,4 @@ ai_sorting_rewards:
js/ai-sorting-rewards.js: {}
dependencies:
- core/drupal
- - core/drupalSettings
\ No newline at end of file
+ - core/drupalSettings
diff --git a/ai_sorting.module b/ai_sorting.module
index b44375c..d0bfca4 100644
--- a/ai_sorting.module
+++ b/ai_sorting.module
@@ -5,6 +5,7 @@
* Primary module file for AI Sorting.
*/
+use Drupal\node\NodeInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\ViewExecutable;
use Drupal\Core\Url;
@@ -17,7 +18,10 @@ function ai_sorting_help($route_name, RouteMatchInterface $route_match) {
case 'help.page.ai_sorting':
$output = '';
$output .= '
' . t('About') . '
';
- $output .= '' . t('The AI Sorting module provides an intelligent sorting mechanism for Drupal Views using reinforcement learning algorithms.') . '
';
+ $output .= '' . t(
+ 'The AI Sorting module provides an intelligent sorting mechanism
+ for Drupal Views using reinforcement learning algorithms.'
+ ) . '
';
return $output;
}
}
@@ -32,44 +36,50 @@ function ai_sorting_views_pre_render(ViewExecutable $view) {
return;
}
- // Collect node IDs and URLs for JavaScript tracking
+ // Collect node IDs and URLs for JavaScript tracking.
$nids = [];
$nid_url_map = [];
-
+
foreach ($view->result as $index => $row) {
-
+
$nid = NULL;
-
- // Try different ways to get the node ID
+
+ // Try different ways to get the node ID.
if (isset($row->nid)) {
$nid = $row->nid;
- } elseif (isset($row->node_field_data_nid)) {
+ }
+ elseif (isset($row->node_field_data_nid)) {
$nid = $row->node_field_data_nid;
- } elseif (isset($row->_entity) && $row->_entity instanceof \Drupal\node\NodeInterface) {
+ }
+ elseif (isset($row->_entity) && $row->_entity instanceof NodeInterface) {
$nid = $row->_entity->id();
- } elseif (isset($row->_object) && method_exists($row->_object, 'id')) {
+ }
+ elseif (isset($row->_object) && method_exists($row->_object, 'id')) {
$nid = $row->_object->id();
}
-
+
if ($nid) {
$nids[] = $nid;
- $nid_url_map[$nid] = Url::fromRoute('entity.node.canonical', ['node' => $nid])->toString();
+ $nid_url_map[$nid] = Url::fromRoute('entity.node.canonical',
+ ['node' => $nid])->toString();
}
}
if (!empty($nids)) {
- // Generate experiment UUID from view and display
+ // Generate experiment UUID from view and display.
$experiment_uuid = sha1($view->id() . ':' . $view->current_display);
- // Register this experiment with the RL module
- $registration_service = \Drupal::service('ai_sorting.experiment_registration');
+ // Register this experiment with the RL module.
+ $registration_service = \Drupal::service(
+ 'ai_sorting.experiment_registration'
+ );
$registration_service->registerExperiment($experiment_uuid);
- // Get RL module path for optimized endpoint
+ // Get RL module path for optimized endpoint.
$rl_path = \Drupal::service('extension.list.module')->getPath('rl');
$base_path = \Drupal::request()->getBasePath();
- // Attach JavaScript libraries and settings
+ // Attach JavaScript libraries and settings.
$view->element['#attached']['library'][] = 'ai_sorting/ai_sorting_turns';
$view->element['#attached']['library'][] = 'ai_sorting/ai_sorting_rewards';
$view->element['#attached']['drupalSettings']['aiSorting']['views'][$view->id()] = [
@@ -93,4 +103,4 @@ function ai_sorting_views_data_alter(array &$data) {
'id' => 'ai_sorting',
],
];
-}
\ No newline at end of file
+}
diff --git a/ai_sorting.permissions.yml b/ai_sorting.permissions.yml
index 89f3f43..8cf3fa3 100644
--- a/ai_sorting.permissions.yml
+++ b/ai_sorting.permissions.yml
@@ -1,3 +1,3 @@
administer ai sorting:
title: 'Administer AI Sorting'
- description: 'Configure AI sorting parameters in Views'
\ No newline at end of file
+ description: 'Configure AI sorting parameters in Views'
diff --git a/ai_sorting.services.yml b/ai_sorting.services.yml
index 00e75f9..51508ef 100644
--- a/ai_sorting.services.yml
+++ b/ai_sorting.services.yml
@@ -15,4 +15,3 @@ services:
ai_sorting.experiment_registration:
class: Drupal\ai_sorting\Service\ExperimentRegistrationService
arguments: ['@rl.experiment_registry']
-
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..091c2a1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+services:
+
+ drupal-lint:
+ image: composer:2.7
+ profiles: ["lint"]
+ working_dir: /src
+ command: bash -c "./scripts/run-drupal-lint.sh"
+ environment:
+ TARGET_DRUPAL_CORE_VERSION: 11
+ volumes:
+ - .:/src
+
+ drupal-lint-auto-fix:
+ image: composer:2.7
+ profiles: ["lint"]
+ working_dir: /src
+ command: bash -c "./scripts/run-drupal-lint-auto-fix.sh"
+ environment:
+ TARGET_DRUPAL_CORE_VERSION: 11
+ volumes:
+ - .:/src
+
+ drupal-check:
+ image: composer:2.7
+ profiles: ["lint"]
+ working_dir: /
+ command: bash -c "/src/scripts/run-drupal-check.sh"
+ tty: true
+ environment:
+ DRUPAL_RECOMMENDED_PROJECT: 11.2.x-dev
+ volumes:
+ - .:/src
diff --git a/docs/ai_sorting_project_desc.html b/docs/ai_sorting_project_desc.html
index 2abdc55..903b755 100644
--- a/docs/ai_sorting_project_desc.html
+++ b/docs/ai_sorting_project_desc.html
@@ -1,6 +1,17 @@
-About AI Sorting
+This module is part of the AI module ecosystem and included in DXPR CMS.
+
+Transform your content marketing with AI Sorting - the Drupal module that automatically learns which content engages your audience and promotes it for maximum impact. Stop guessing what works and let machine learning optimize your content visibility.
-Transform your content marketing with AI Sorting - the Drupal module that automatically learns which content engages your audience and promotes it for maximum impact. Stop guessing what works and let machine learning optimize your content visibility.
+You need AI Sorting if
+
+ - You want your best-performing content to automatically rise to the top
+ - You need machine learning to optimize content visibility without manual work
+ - You manage blogs, news feeds, or product catalogs with varying engagement
+ - You want real-time content optimization based on user interactions
+ - You prefer data-driven content ranking over manual curation
+
+
+About AI Sorting
Boost Your Content Performance
@@ -68,6 +79,12 @@ Enterprise-Ready
Mobile Responsive - Works across all devices and platforms
+
+
Prefer a turnkey demo site?
+
Spin up DXPR CMS—Drupal pre-configured with DXPR Builder, DXPR Theme, AI Sorting module, and security best practices.
+
Get DXPR CMS »
+
+
Dependencies
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..8ffbb22
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,22 @@
+
+
+ Drupal 11 coding standards for module.
+ .
+
+
+
+ */.git/*
+ */config/*
+ */css/*
+ */js/*
+ */node_modules/*
+ */vendor/*
+ */.github/*
+
+
+
+
+
+
+
+
diff --git a/scripts/prepare-drupal-lint.sh b/scripts/prepare-drupal-lint.sh
new file mode 100755
index 0000000..7f2592a
--- /dev/null
+++ b/scripts/prepare-drupal-lint.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+set -e
+
+if [ -z "$TARGET_DRUPAL_CORE_VERSION" ]; then
+ # default to target Drupal 11, you can override this by setting the secrets value on your github repo
+ TARGET_DRUPAL_CORE_VERSION=11
+fi
+
+echo "php --version"
+php --version
+echo "composer --version"
+composer --version
+
+echo "\$COMPOSER_HOME: $COMPOSER_HOME"
+echo "TARGET_DRUPAL_CORE_VERSION: $TARGET_DRUPAL_CORE_VERSION"
+
+# Add this line to avoid the plugin prompt
+composer config --global allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
+
+composer global require drupal/coder --dev
+composer global require phpcompatibility/php-compatibility --dev
+
+export PATH="$PATH:$COMPOSER_HOME/vendor/bin"
+
+composer global require dealerdirect/phpcodesniffer-composer-installer --dev
+
+composer global show -P
+phpcs -i
+
+phpcs --config-set colors 1
+phpcs --config-set drupal_core_version 11$TARGET_DRUPAL_CORE_VERSION
+
+phpcs --config-show
\ No newline at end of file
diff --git a/scripts/run-drupal-check.sh b/scripts/run-drupal-check.sh
new file mode 100755
index 0000000..41ee929
--- /dev/null
+++ b/scripts/run-drupal-check.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -vo pipefail
+
+DRUPAL_RECOMMENDED_PROJECT=${DRUPAL_RECOMMENDED_PROJECT:-10.3.x-dev}
+PHP_EXTENSIONS="gd"
+DRUPAL_CHECK_TOOL="mglaman/drupal-check"
+
+# Install required PHP extensions
+for ext in $PHP_EXTENSIONS; do
+ if ! php -m | grep -q $ext; then
+ apk update && apk add --no-cache ${ext}-dev
+ docker-php-ext-install $ext
+ fi
+done
+
+# Create Drupal project if it doesn't exist
+if [ ! -d "/drupal" ]; then
+ composer create-project drupal/recommended-project=$DRUPAL_RECOMMENDED_PROJECT drupal --no-interaction --stability=dev
+fi
+
+cd drupal
+mkdir -p web/modules/contrib/
+
+# Symlink analyze_ai_brand_voice if not already linked
+if [ ! -L "web/modules/contrib/analyze_ai_brand_voice" ]; then
+ ln -s /src web/modules/contrib/analyze_ai_brand_voice
+fi
+
+# Install the statistic modules if D11 (removed from core).
+if [[ $DRUPAL_RECOMMENDED_PROJECT == 11.* ]]; then
+ composer require drupal/statistics
+fi
+
+# Install drupal-check
+composer require $DRUPAL_CHECK_TOOL --dev
+
+# Run drupal-check
+./vendor/bin/drupal-check --drupal-root . -ad web/modules/contrib/analyze_ai_brand_voice
\ No newline at end of file
diff --git a/scripts/run-drupal-lint-auto-fix.sh b/scripts/run-drupal-lint-auto-fix.sh
new file mode 100755
index 0000000..f796ef5
--- /dev/null
+++ b/scripts/run-drupal-lint-auto-fix.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source scripts/prepare-drupal-lint.sh
+
+phpcbf --standard=Drupal \
+ --extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
+ --ignore=node_modules,ai_sorting/vendor,.github,vendor \
+ .
+
+phpcbf --standard=DrupalPractice \
+ --extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
+ --ignore=node_modules,ai_sorting/vendor,.github,vendor \
+ .
\ No newline at end of file
diff --git a/scripts/run-drupal-lint.sh b/scripts/run-drupal-lint.sh
new file mode 100755
index 0000000..1624b52
--- /dev/null
+++ b/scripts/run-drupal-lint.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+source scripts/prepare-drupal-lint.sh
+
+EXIT_CODE=0
+
+echo "---- Checking with PHPCompatibility PHP 8.3 and up ----"
+phpcs --standard=PHPCompatibility \
+ --runtime-set testVersion 8.3- \
+ --extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
+ --ignore=node_modules,ai_sorting/vendor,.github,vendor \
+ -v \
+ .
+status=$?
+if [ $status -ne 0 ]; then
+ EXIT_CODE=$status
+fi
+
+echo "---- Checking with Drupal standard... ----"
+phpcs --standard=Drupal \
+ --extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
+ --ignore=node_modules,ai_sorting/vendor,.github,vendor \
+ -v \
+ .
+status=$?
+if [ $status -ne 0 ]; then
+ EXIT_CODE=$status
+fi
+
+echo "---- Checking with DrupalPractice standard... ----"
+phpcs --standard=DrupalPractice \
+ --extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
+ --ignore=node_modules,ai_sorting/vendor,.github,vendor \
+ -v \
+ .
+
+status=$?
+if [ $status -ne 0 ]; then
+ EXIT_CODE=$status
+fi
+
+# Exit with failure if any of the checks failed
+exit $EXIT_CODE
\ No newline at end of file
diff --git a/src/Decorator/AiSortingExperimentDecorator.php b/src/Decorator/AiSortingExperimentDecorator.php
index c31cd29..3b035f4 100644
--- a/src/Decorator/AiSortingExperimentDecorator.php
+++ b/src/Decorator/AiSortingExperimentDecorator.php
@@ -6,7 +6,6 @@
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\rl\Decorator\ExperimentDecoratorInterface;
-use Drupal\views\ViewEntityInterface;
/**
* Decorator service for AI Sorting experiments.
@@ -43,10 +42,10 @@ public function decorateExperiment(string $uuid): ?array {
foreach ($view->get('display') as $display_id => $display) {
$test_uuid = sha1($view->id() . ':' . $display_id);
if ($test_uuid === $uuid) {
- // Found matching view and display
+ // Found matching view and display.
$view_url = Url::fromRoute('entity.view.edit_form', ['view' => $view->id()]);
$view_link = Link::fromTextAndUrl($view->label(), $view_url);
-
+
return [
'#markup' => $view_link->toString() . ' (' . $display_id . ')',
];
@@ -61,25 +60,25 @@ public function decorateExperiment(string $uuid): ?array {
* {@inheritdoc}
*/
public function decorateArm(string $experiment_uuid, string $arm_id): ?array {
- // For AI Sorting, arm_id should be a node ID
+ // For AI Sorting, arm_id should be a node ID.
if (is_numeric($arm_id)) {
try {
$node = $this->entityTypeManager->getStorage('node')->load($arm_id);
if ($node) {
$node_url = Url::fromRoute('entity.node.canonical', ['node' => $node->id()]);
$node_link = Link::fromTextAndUrl($node->label(), $node_url);
-
+
return [
'#markup' => $node_link->toString(),
];
}
}
catch (\Exception $e) {
- // If node loading fails, fall back to raw arm_id
+ // If node loading fails, fall back to raw arm_id.
}
}
return NULL;
}
-}
\ No newline at end of file
+}
diff --git a/src/Plugin/views/sort/AISorting.php b/src/Plugin/views/sort/AISorting.php
index 9cb28eb..91136f9 100644
--- a/src/Plugin/views/sort/AISorting.php
+++ b/src/Plugin/views/sort/AISorting.php
@@ -9,7 +9,6 @@
use Drupal\rl\Service\ExperimentManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
@@ -82,8 +81,11 @@ public static function create(ContainerInterface $container, array $configuratio
*/
protected function defineOptions() {
$options = parent::defineOptions();
- $options['order'] = ['default' => '']; // We don't use this, but prevents warnings.
- $options['cache_max_age'] = ['default' => 60]; // Default max-age set to 60 seconds.
+ $options['order'] = ['default' => ''];
+ $options['cache_max_age'] = ['default' => 60];
+ $options['favor_recent'] = ['default' => FALSE];
+ // 3 months default
+ $options['time_window_seconds'] = ['default' => 7776000];
return $options;
}
@@ -94,54 +96,55 @@ public function query() {
try {
$this->ensureMyTable();
- // Generate experiment UUID from view and display
$experiment_uuid = sha1($this->view->id() . ':' . $this->view->current_display);
- // Get Thompson Sampling scores from RL module
- $scores = $this->experimentManager->getUCB1Scores($experiment_uuid);
+ // Only apply time window if "Favor recent content" is enabled.
+ $time_window_seconds = NULL;
+ if ($this->options['favor_recent']) {
+ $time_window_seconds = $this->options['time_window_seconds'];
+ }
+
+ $scores = $this->experimentManager->getThompsonScoresWithWindow(
+ $experiment_uuid,
+ $time_window_seconds
+ );
if (empty($scores)) {
- // No data yet, fall back to random order
- $this->query->addOrderBy(NULL, 'RAND()', 'DESC', 'ai_sorting_fallback');
- return;
+ throw new \RuntimeException(sprintf(
+ 'No scores for experiment "%s". Check RL tracking.',
+ $experiment_uuid
+ ));
}
- // Build a CASE statement to order by UCB1 scores
$case_statement = 'CASE ' . $this->tableAlias . '.nid ';
foreach ($scores as $nid => $score) {
$case_statement .= "WHEN " . (int) $nid . " THEN " . (float) $score . " ";
}
$case_statement .= 'ELSE 0 END';
- // Add small random noise to break ties
- $order_formula = $case_statement . ' + (RAND() * 0.000001)';
-
$this->query->addOrderBy(
NULL,
- $order_formula,
+ $case_statement,
'DESC',
'ai_sorting_score'
);
- // Disable dynamic page cache for AI sorting
\Drupal::service('page_cache_kill_switch')->trigger();
- } catch (\Exception $e) {
+ }
+ catch (\Exception $e) {
$logger = $this->loggerFactory->get('ai_sorting');
- $logger->error('Error in AI Sorting query(): @message', ['@message' => $e->getMessage()]);
- $logger->error('Stack trace: @trace', ['@trace' => $e->getTraceAsString()]);
+ $logger->error('AI Sorting: @message', ['@message' => $e->getMessage()]);
throw $e;
}
}
-
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
-
- // Remove the order selector since we always use DESC for UCB1 scores
+
unset($form['order']);
$form['ai_sorting_settings'] = [
@@ -153,8 +156,53 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
How it works:
• Turns: When content appears in this view
• Rewards: When users click on that content
- • The algorithm balances showing popular content with exploring new options
- Best for: News feeds, product listings, blog posts, or any content where user engagement matters.'),
+ • The algorithm balances showing popular content with exploring new options.'),
+ ];
+
+ $form['ai_sorting_settings']['favor_recent'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Favor recent content'),
+ '#default_value' => $this->options['favor_recent'] ?? FALSE,
+ '#description' => $this->t('Enable this for content that becomes outdated (news, campaigns, seasonal products).'),
+ ];
+
+ $form['ai_sorting_settings']['time_window_seconds'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Count interactions from'),
+ '#default_value' => $this->options['time_window_seconds'] ?? 7776000,
+ '#options' => [
+ // 30 * 24 * 60 * 60
+ 2592000 => $this->t('Last month'),
+ // 90 * 24 * 60 * 60
+ 7776000 => $this->t('Last 3 months'),
+ // 180 * 24 * 60 * 60
+ 15552000 => $this->t('Last 6 months'),
+ // 365 * 24 * 60 * 60
+ 31536000 => $this->t('Last year'),
+ ],
+ '#states' => [
+ 'visible' => [
+ ':input[name="options[ai_sorting_settings][favor_recent]"]' => ['checked' => TRUE],
+ ],
+ ],
+ '#description' => $this->t('Only recently active content influences recommendations.'),
+ ];
+
+ $form['ai_sorting_settings']['help'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Which timeframe should I choose?'),
+ '#open' => FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="options[ai_sorting_settings][favor_recent]"]' => ['checked' => TRUE],
+ ],
+ ],
+ '#description' => $this->t('
+ News & announcements: Last month
+ Blog posts: Last 3 months
+ Products: Last 6 months
+ Leave unchecked for: Documentation, tutorials, evergreen content
+ '),
];
$url = Url::fromUri('https://en.wikipedia.org/wiki/Thompson_sampling', [
@@ -165,14 +213,12 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
]);
$link = Link::fromTextAndUrl($this->t('Learn more about Thompson Sampling'), $url);
- // Add an advanced details element for cache settings.
$form['ai_sorting_settings']['advanced'] = [
'#type' => 'details',
'#title' => $this->t('Advanced Settings'),
'#open' => FALSE,
];
-
$form['ai_sorting_settings']['advanced']['cache_max_age'] = [
'#type' => 'select',
'#title' => $this->t('Browser and proxy cache maximum age'),
@@ -198,38 +244,42 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) {
$options = &$form_state->getValue('options');
- // Save the cache_max_age value
+ if (isset($options['ai_sorting_settings']['favor_recent'])) {
+ $this->options['favor_recent'] = $options['ai_sorting_settings']['favor_recent'];
+ }
+
+ if (isset($options['ai_sorting_settings']['time_window_seconds'])) {
+ $this->options['time_window_seconds'] = $options['ai_sorting_settings']['time_window_seconds'];
+ }
+
if (isset($options['ai_sorting_settings']['advanced']['cache_max_age'])) {
$this->options['cache_max_age'] = $options['ai_sorting_settings']['advanced']['cache_max_age'];
}
- // Auto-configure views cache to match AI sorting settings
$cache_max_age = $this->options['cache_max_age'] ?? 60;
$current_cache = $this->view->display_handler->getOption('cache');
-
+
if ($cache_max_age > 0) {
- // Set time-based cache matching our AI sorting refresh rate
if ($current_cache['type'] !== 'time' || $current_cache['options']['output_lifespan'] != $cache_max_age) {
$this->view->display_handler->setOption('cache', [
'type' => 'time',
'options' => [
'output_lifespan' => $cache_max_age,
'results_lifespan' => $cache_max_age,
- ]
+ ],
]);
-
+
\Drupal::messenger()->addStatus($this->t('Views cache has been automatically set to @seconds seconds to match your AI sorting refresh rate.', ['@seconds' => $cache_max_age]));
}
- } else {
- // Disable cache when AI sorting cache is set to 0
+ }
+ else {
if ($current_cache['type'] !== 'none') {
$this->view->display_handler->setOption('cache', ['type' => 'none']);
-
+
\Drupal::messenger()->addWarning($this->t('Views cache has been automatically disabled because AI sorting cache is set to "Never cache".'));
}
}
- // Clear any caches if necessary
\Drupal::service('plugin.manager.views.sort')->clearCachedDefinitions();
}
@@ -238,21 +288,46 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) {
*/
public function adminSummary() {
$summary = [];
-
+
+ // Time window summary.
+ if (!empty($this->options['favor_recent'])) {
+ $time_window_seconds = $this->options['time_window_seconds'];
+ if ($time_window_seconds == 2592000) {
+ $summary[] = $this->t('Time window: Last month');
+ }
+ elseif ($time_window_seconds == 7776000) {
+ $summary[] = $this->t('Time window: Last 3 months');
+ }
+ elseif ($time_window_seconds == 15552000) {
+ $summary[] = $this->t('Time window: Last 6 months');
+ }
+ elseif ($time_window_seconds == 31536000) {
+ $summary[] = $this->t('Time window: Last year');
+ }
+ else {
+ $days = round($time_window_seconds / 86400);
+ $summary[] = $this->t('Time window: Last @days days', ['@days' => $days]);
+ }
+ }
+
+ // Cache summary.
$cache_max_age = $this->options['cache_max_age'];
if ($cache_max_age == 0) {
$summary[] = $this->t('Cache: Never cache');
- } elseif ($cache_max_age < 60) {
+ }
+ elseif ($cache_max_age < 60) {
$summary[] = $this->t('Cache: @seconds seconds', ['@seconds' => $cache_max_age]);
- } elseif ($cache_max_age < 3600) {
+ }
+ elseif ($cache_max_age < 3600) {
$minutes = $cache_max_age / 60;
$summary[] = $this->t('Cache: @minutes minute(s)', ['@minutes' => $minutes]);
- } else {
+ }
+ else {
$hours = $cache_max_age / 3600;
$summary[] = $this->t('Cache: @hours hour(s)', ['@hours' => $hours]);
}
-
+
return implode(', ', $summary);
}
-}
\ No newline at end of file
+}
diff --git a/src/Service/ExperimentRegistrationService.php b/src/Service/ExperimentRegistrationService.php
index 54c2b5c..329bcf3 100644
--- a/src/Service/ExperimentRegistrationService.php
+++ b/src/Service/ExperimentRegistrationService.php
@@ -36,4 +36,4 @@ public function registerExperiment(string $uuid): void {
$this->experimentRegistry->register($uuid, 'ai_sorting');
}
-}
\ No newline at end of file
+}
diff --git a/src/Service/ExperimentResolver.php b/src/Service/ExperimentResolver.php
index f094edb..0b8f200 100644
--- a/src/Service/ExperimentResolver.php
+++ b/src/Service/ExperimentResolver.php
@@ -40,4 +40,4 @@ public function extractViewInfo($experiment_uuid) {
return NULL;
}
-}
\ No newline at end of file
+}
diff --git a/src/Service/TotalTrialsService.php b/src/Service/TotalTrialsService.php
index e6e3dc5..d04ea81 100644
--- a/src/Service/TotalTrialsService.php
+++ b/src/Service/TotalTrialsService.php
@@ -57,4 +57,4 @@ public function getTotalTrials($view_id, $display_id) {
return $this->experimentManager->getTotalTurns($experiment_uuid);
}
-}
\ No newline at end of file
+}