diff --git a/analyze_ai_sentiments.install b/analyze_ai_sentiments.install index 328ffaf..409a1f9 100644 --- a/analyze_ai_sentiments.install +++ b/analyze_ai_sentiments.install @@ -286,3 +286,243 @@ function analyze_ai_sentiments_update_8002() { return t('Updated view filters and submit button.'); } + +/** + * Add view fields, filters, enable time_diff, and standardize display. + */ +function analyze_ai_sentiments_update_8003() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('views.view.ai_sentiments_analysis_results'); + + if ($config->isNew()) { + return t('View not found.'); + } + + $changes = []; + + // Add content type filter if not present. + $filters = $config->get('display.default.display_options.filters') ?? []; + if (!isset($filters['type'])) { + $type_filter = [ + 'id' => 'type', + 'table' => 'node_field_data', + 'field' => 'type', + 'relationship' => 'entity_id', + 'group_type' => 'group', + 'admin_label' => '', + 'entity_type' => 'node', + 'entity_field' => 'type', + 'plugin_id' => 'bundle', + 'operator' => 'in', + 'value' => [], + 'group' => 1, + 'exposed' => TRUE, + 'expose' => [ + 'operator_id' => 'type_op', + 'label' => 'Content type', + 'description' => '', + 'use_operator' => FALSE, + 'operator' => 'type_op', + 'operator_limit_selection' => FALSE, + 'operator_list' => [], + 'identifier' => 'type', + 'required' => FALSE, + 'remember' => FALSE, + 'multiple' => FALSE, + 'remember_roles' => ['authenticated' => 'authenticated'], + 'reduce' => FALSE, + ], + 'is_grouped' => FALSE, + 'group_info' => [ + 'label' => '', + 'description' => '', + 'identifier' => '', + 'optional' => TRUE, + 'widget' => 'select', + 'multiple' => FALSE, + 'remember' => FALSE, + 'default_group' => 'All', + 'default_group_multiple' => [], + 'group_items' => [], + ], + ]; + $filters = ['type' => $type_filter] + $filters; + $config->set('display.default.display_options.filters', $filters); + $changes[] = 'content type filter'; + } + + // Define field templates. + $alter_defaults = [ + 'alter_text' => FALSE, + 'text' => '', + 'make_link' => FALSE, + 'path' => '', + 'absolute' => FALSE, + 'external' => FALSE, + 'replace_spaces' => FALSE, + 'path_case' => 'none', + 'trim_whitespace' => FALSE, + 'alt' => '', + 'rel' => '', + 'link_class' => '', + 'prefix' => '', + 'suffix' => '', + 'target' => '', + 'nl2br' => FALSE, + 'max_length' => 0, + 'word_boundary' => TRUE, + 'ellipsis' => TRUE, + 'more_link' => FALSE, + 'more_link_text' => '', + 'more_link_path' => '', + 'strip_tags' => FALSE, + 'trim' => FALSE, + 'preserve_tags' => '', + 'html' => FALSE, + ]; + + $time_diff_settings = [ + 'date_format' => 'medium', + 'custom_date_format' => '', + 'timezone' => '', + 'tooltip' => ['date_format' => 'long', 'custom_date_format' => ''], + 'time_diff' => [ + 'enabled' => TRUE, + 'future_format' => '@interval hence', + 'past_format' => '@interval ago', + 'granularity' => 1, + 'refresh' => 60, + ], + ]; + + $fields = $config->get('display.default.display_options.fields') ?? []; + $columns = $config->get('display.default.display_options.style.options.columns') ?? []; + $info = $config->get('display.default.display_options.style.options.info') ?? []; + + // Add created field if not present. + if (!isset($fields['created'])) { + $fields['created'] = [ + 'id' => 'created', + 'table' => 'node_field_data', + 'field' => 'created', + 'relationship' => 'entity_id', + 'group_type' => 'group', + 'admin_label' => '', + 'entity_type' => 'node', + 'entity_field' => 'created', + 'plugin_id' => 'field', + 'label' => 'Created', + 'exclude' => FALSE, + 'alter' => $alter_defaults, + 'element_type' => '', + 'element_class' => '', + 'element_label_type' => '', + 'element_label_class' => '', + 'element_label_colon' => TRUE, + 'element_wrapper_type' => '', + 'element_wrapper_class' => '', + 'element_default_classes' => TRUE, + 'empty' => '', + 'hide_empty' => FALSE, + 'empty_zero' => FALSE, + 'hide_alter_empty' => TRUE, + 'click_sort_column' => 'value', + 'type' => 'timestamp', + 'settings' => $time_diff_settings, + 'group_column' => 'value', + 'group_columns' => [], + 'group_rows' => TRUE, + 'delta_limit' => 0, + 'delta_offset' => 0, + 'delta_reversed' => FALSE, + 'delta_first_last' => FALSE, + 'multi_type' => 'separator', + 'separator' => ', ', + 'field_api_classes' => FALSE, + ]; + $columns['created'] = 'created'; + $info['created'] = [ + 'sortable' => TRUE, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => FALSE, + 'responsive' => '', + ]; + $changes[] = 'created field'; + } + + // Enable time_diff on changed field if present. + if (isset($fields['changed'])) { + $fields['changed']['settings'] = $time_diff_settings; + $fields['changed']['label'] = 'Last edited'; + $changes[] = 'time_diff on changed field'; + } + + // Add analyzed_timestamp field if not present. + if (!isset($fields['analyzed_timestamp'])) { + $fields['analyzed_timestamp'] = [ + 'id' => 'analyzed_timestamp', + 'table' => 'analyze_ai_sentiments_results', + 'field' => 'analyzed_timestamp', + 'relationship' => 'none', + 'group_type' => 'group', + 'admin_label' => '', + 'plugin_id' => 'date', + 'label' => 'Last analyzed', + 'exclude' => FALSE, + 'date_format' => 'time ago', + 'custom_date_format' => '', + 'timezone' => '', + 'alter' => $alter_defaults, + 'element_type' => '', + 'element_class' => '', + 'element_label_type' => '', + 'element_label_class' => '', + 'element_label_colon' => TRUE, + 'element_wrapper_type' => '', + 'element_wrapper_class' => '', + 'element_default_classes' => TRUE, + 'empty' => '', + 'hide_empty' => FALSE, + 'empty_zero' => FALSE, + 'hide_alter_empty' => TRUE, + 'click_sort_column' => 'value', + 'group_column' => 'value', + 'group_columns' => [], + 'group_rows' => TRUE, + 'delta_limit' => 0, + 'delta_offset' => 0, + 'delta_reversed' => FALSE, + 'delta_first_last' => FALSE, + 'multi_type' => 'separator', + 'separator' => ', ', + 'field_api_classes' => FALSE, + ]; + $columns['analyzed_timestamp'] = 'analyzed_timestamp'; + $info['analyzed_timestamp'] = [ + 'sortable' => TRUE, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => FALSE, + 'responsive' => '', + ]; + $changes[] = 'analyzed_timestamp field'; + } + else { + // Set time ago format on analyzed_timestamp. + // Views Date plugin uses date_format option. + $fields['analyzed_timestamp']['date_format'] = 'time ago'; + $fields['analyzed_timestamp']['custom_date_format'] = ''; + $fields['analyzed_timestamp']['timezone'] = ''; + $changes[] = 'time ago format on analyzed_timestamp'; + } + + $config->set('display.default.display_options.fields', $fields); + $config->set('display.default.display_options.style.options.columns', $columns); + $config->set('display.default.display_options.style.options.info', $info); + $config->save(); + + return t('Updated: @changes.', ['@changes' => implode(', ', $changes)]); +} diff --git a/config/install/views.view.ai_sentiments_analysis_results.yml b/config/install/views.view.ai_sentiments_analysis_results.yml index a8a2549..b08409b 100644 --- a/config/install/views.view.ai_sentiments_analysis_results.yml +++ b/config/install/views.view.ai_sentiments_analysis_results.yml @@ -7,6 +7,7 @@ dependencies: module: - analyze_ai_sentiments - node + - user - views_color_scales _core: default_config_hash: N0NS4fONEuTHqrzhxLvORys7CNEVqlCaEY5Q8g-urs4 @@ -69,7 +70,7 @@ display: element_class: '' element_label_type: '' element_label_class: '' - element_label_colon: false + element_label_colon: true element_wrapper_type: '' element_wrapper_class: '' element_default_classes: true @@ -203,6 +204,83 @@ display: color_scale_auto: 0 color_scale_min_color: '#FFB3B3' color_scale_max_color: '#B3FFB3' + analyzed_timestamp: + id: analyzed_timestamp + table: analyze_ai_sentiments_results + field: analyzed_timestamp + relationship: none + group_type: group + admin_label: '' + plugin_id: date + label: 'Last analyzed' + exclude: false + date_format: 'time ago' + custom_date_format: '' + timezone: '' + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: medium + custom_date_format: '' + timezone: '' + tooltip: + date_format: long + custom_date_format: '' + time_diff: + enabled: true + future_format: '@interval hence' + past_format: '@interval ago' + granularity: 1 + refresh: 60 + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false type: id: type table: node_field_data @@ -322,19 +400,94 @@ display: click_sort_column: value type: timestamp settings: - date_format: html_datetime + date_format: medium custom_date_format: '' timezone: '' tooltip: date_format: long custom_date_format: '' time_diff: - enabled: false + enabled: true + future_format: '@interval hence' + past_format: '@interval ago' + granularity: 1 + refresh: 60 + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + created: + id: created + table: node_field_data + field: created + relationship: entity_id + group_type: group + admin_label: '' + entity_type: node + entity_field: created + plugin_id: field + label: Created + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: medium + custom_date_format: '' + timezone: '' + tooltip: + date_format: '' + custom_date_format: '' + time_diff: + enabled: true future_format: '@interval hence' past_format: '@interval ago' - granularity: 2 + granularity: 1 refresh: 60 - description: '' group_column: value group_columns: { } group_rows: true @@ -448,15 +601,15 @@ display: options: { } empty: { } sorts: - created: - id: created + changed: + id: changed table: node_field_data - field: created - relationship: none + field: changed + relationship: entity_id group_type: group admin_label: '' entity_type: node - entity_field: created + entity_field: changed plugin_id: date order: DESC expose: @@ -466,6 +619,47 @@ display: granularity: second arguments: { } filters: + type: + id: type + table: node_field_data + field: type + relationship: entity_id + group_type: group + admin_label: '' + entity_type: node + entity_field: type + plugin_id: bundle + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Content type' + description: '' + use_operator: false + operator: type_op + operator_limit_selection: false + operator_list: { } + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } langcode: id: langcode table: node_field_data @@ -534,50 +728,19 @@ display: multiple: false remember_roles: authenticated: authenticated - anonymous: '0' - marketer: '0' - administrator: '0' - dxpr_enterprise: '0' - marketing: '0' - sooperthemes_enterprise: '0' - stoppedsubscription: '0' - t1_support: '0' - t2_support: '0' - business: '0' - dxpr_business: '0' - dxpr_personal: '0' - dxpr_professional: '0' - growth: '0' - legacy_agency_membership: '0' - pre-sooperthemes_membership: '0' - sooperthemes_professional: '0' - sooperthemes_single: '0' - standard: '0' placeholder: '' is_grouped: false group_info: - label: 'Sentiments Type' + label: '' description: '' - identifier: sentiments_id + identifier: '' optional: true widget: select multiple: false remember: false default_group: All default_group_multiple: { } - group_items: - 1: - title: '' - operator: '=' - value: '' - 2: - title: '' - operator: '=' - value: '' - 3: - title: '' - operator: '=' - value: '' + group_items: { } score: id: score table: analyze_ai_sentiments_results @@ -597,35 +760,16 @@ display: operator_id: score_op label: 'Sentiments Score' description: '' - use_operator: true + use_operator: false operator: score_op operator_limit_selection: false operator_list: { } identifier: score required: false - remember: true + remember: false multiple: false remember_roles: authenticated: authenticated - anonymous: '0' - marketer: '0' - administrator: '0' - dxpr_enterprise: '0' - marketing: '0' - sooperthemes_enterprise: '0' - stoppedsubscription: '0' - t1_support: '0' - t2_support: '0' - business: '0' - dxpr_business: '0' - dxpr_personal: '0' - dxpr_professional: '0' - growth: '0' - legacy_agency_membership: '0' - pre-sooperthemes_membership: '0' - sooperthemes_professional: '0' - sooperthemes_single: '0' - standard: '0' min_placeholder: '' max_placeholder: '' placeholder: '' @@ -647,7 +791,7 @@ display: field: title relationship: entity_id group_type: group - admin_label: Search + admin_label: '' entity_type: node entity_field: title plugin_id: string @@ -663,31 +807,12 @@ display: operator: title_op operator_limit_selection: false operator_list: { } - identifier: Search + identifier: title required: false - remember: true + remember: false multiple: false remember_roles: authenticated: authenticated - anonymous: '0' - marketer: '0' - administrator: '0' - dxpr_enterprise: '0' - marketing: '0' - sooperthemes_enterprise: '0' - stoppedsubscription: '0' - t1_support: '0' - t2_support: '0' - business: '0' - dxpr_business: '0' - dxpr_personal: '0' - dxpr_professional: '0' - growth: '0' - legacy_agency_membership: '0' - pre-sooperthemes_membership: '0' - sooperthemes_professional: '0' - sooperthemes_single: '0' - standard: '0' placeholder: '' is_grouped: false group_info: @@ -715,10 +840,12 @@ display: title: title sentiments_id: sentiments_id score: score + analyzed_timestamp: analyzed_timestamp type: type + created: created changed: changed uid: uid - default: '-1' + default: analyzed_timestamp info: title: sortable: true @@ -741,6 +868,13 @@ display: separator: '' empty_column: false responsive: '' + analyzed_timestamp: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' type: sortable: true default_sort_order: asc @@ -748,6 +882,13 @@ display: separator: '' empty_column: false responsive: '' + created: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' changed: sortable: true default_sort_order: asc @@ -803,6 +944,7 @@ display: - 'languages:language_interface' - url - url.query_args + - user.permissions tags: { } page_1: id: page_1 @@ -817,7 +959,7 @@ display: type: normal title: 'AI Sentiments Analysis' description: 'View detailed sentiments scores and content analysis metrics for all analyzed entities.' - expanded: true + expanded: false menu_name: admin parent: system.admin_reports context: '0' @@ -828,4 +970,5 @@ display: - 'languages:language_interface' - url - url.query_args + - user.permissions tags: { } diff --git a/src/Service/SentimentsStorageService.php b/src/Service/SentimentsStorageService.php index fb87754..264b995 100644 --- a/src/Service/SentimentsStorageService.php +++ b/src/Service/SentimentsStorageService.php @@ -63,43 +63,40 @@ public function getScores(EntityInterface $entity): array { * Array of sentiments_id => score pairs. */ public function saveScores(EntityInterface $entity, array $scores): void { - $content_hash = $this->generateContentHash($entity); - $config_hash = $this->generateConfigHash(); + foreach ($scores as $sentiments_id => $score) { + $this->saveScore($entity, $sentiments_id, $score); + } + } - // Delete existing scores for this entity/language combination. - $this->database->delete('analyze_ai_sentiments_results') - ->condition('entity_type', $entity->getEntityTypeId()) - ->condition('entity_id', $entity->id()) - ->condition('langcode', $entity->language()->getId()) + /** + * Saves a single sentiment score for an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity the score is for. + * @param string $sentiments_id + * The sentiment ID. + * @param float $score + * The sentiment score (-1.0 to +1.0). + */ + public function saveScore(EntityInterface $entity, string $sentiments_id, float $score): void { + // Ensure score is within valid range. + $score = max(-1.0, min(1.0, $score)); + + $this->database->merge('analyze_ai_sentiments_results') + ->keys([ + 'entity_type' => $entity->getEntityTypeId(), + 'entity_id' => $entity->id(), + 'sentiments_id' => $sentiments_id, + 'langcode' => $entity->language()->getId(), + ]) + ->fields([ + 'entity_revision_id' => $entity instanceof RevisionableInterface ? $entity->getRevisionId() : 0, + 'score' => $score, + 'content_hash' => $this->generateContentHash($entity), + 'config_hash' => $this->generateConfigHash(), + 'analyzed_timestamp' => $this->time->getRequestTime(), + ]) ->execute(); - - // Insert new scores. - if (!empty($scores)) { - $insert = $this->database->insert('analyze_ai_sentiments_results') - ->fields([ - 'entity_type', 'entity_id', 'entity_revision_id', 'langcode', - 'sentiments_id', 'score', 'content_hash', 'config_hash', 'analyzed_timestamp', - ]); - - foreach ($scores as $sentiments_id => $score) { - // Ensure score is within valid range. - $score = max(-1.0, min(1.0, (float) $score)); - - $insert->values([ - 'entity_type' => $entity->getEntityTypeId(), - 'entity_id' => $entity->id(), - 'entity_revision_id' => $entity instanceof RevisionableInterface ? $entity->getRevisionId() : NULL, - 'langcode' => $entity->language()->getId(), - 'sentiments_id' => $sentiments_id, - 'score' => $score, - 'content_hash' => $content_hash, - 'config_hash' => $config_hash, - 'analyzed_timestamp' => $this->time->getRequestTime(), - ]); - } - - $insert->execute(); - } } /**