diff --git a/.codeclimate.yml b/.codeclimate.yml index ff19a8b13..3a28cf1b7 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,7 +2,7 @@ version: "2" checks: argument-count: config: - threshold: 8 + threshold: 9 complex-logic: config: threshold: 4 diff --git a/.rubocop.yml b/.rubocop.yml index 2ff1fbf74..78aae1b28 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -243,10 +243,10 @@ Metrics/ModuleLength: Max: 250 Metrics/ParameterLists: - Description: 'Avoid parameter lists longer than three or four parameters.' + Description: 'Avoid parameter lists longer than 9 parameters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' Enabled: true - Max: 8 + Max: 9 Metrics/PerceivedComplexity: Description: >- diff --git a/app/graph/types/team_statistics_type.rb b/app/graph/types/team_statistics_type.rb new file mode 100644 index 000000000..5c98bfddd --- /dev/null +++ b/app/graph/types/team_statistics_type.rb @@ -0,0 +1,40 @@ +class TeamStatisticsType < DefaultObject + description 'Workspace statistics.' + + implements GraphQL::Types::Relay::Node + + # For articles + + field :number_of_articles_created_by_date, JsonStringType, null: true + field :number_of_articles_updated_by_date, JsonStringType, null: true + field :number_of_explainers_created, GraphQL::Types::Int, null: true + field :number_of_fact_checks_created, GraphQL::Types::Int, null: true + field :number_of_published_fact_checks, GraphQL::Types::Int, null: true + field :number_of_fact_checks_by_rating, JsonStringType, null: true + field :top_articles_sent, JsonStringType, null: true + field :top_articles_tags, JsonStringType, null: true + + # For tiplines + + field :number_of_messages, GraphQL::Types::Int, null: true + field :number_of_conversations, GraphQL::Types::Int, null: true + field :number_of_messages_by_date, JsonStringType, null: true + field :number_of_conversations_by_date, JsonStringType, null: true + field :number_of_search_results_by_feedback_type, JsonStringType, null: true + field :average_response_time, GraphQL::Types::Int, null: true + field :number_of_unique_users, GraphQL::Types::Int, null: true + field :number_of_total_users, GraphQL::Types::Int, null: true + field :number_of_returning_users, GraphQL::Types::Int, null: true + field :number_of_subscribers, GraphQL::Types::Int, null: true + field :number_of_new_subscribers, GraphQL::Types::Int, null: true + field :number_of_newsletters_sent, GraphQL::Types::Int, null: true + field :number_of_newsletters_delivered, GraphQL::Types::Int, null: true + field :top_media_tags, JsonStringType, null: true + field :top_requested_media_clusters, JsonStringType, null: true + field :number_of_media_received_by_media_type, JsonStringType, null: true + + # For both articles and tiplines + + field :number_of_articles_sent, GraphQL::Types::Int, null: true + field :number_of_matched_results_by_article_type, JsonStringType, null: true +end diff --git a/app/graph/types/team_type.rb b/app/graph/types/team_type.rb index 97c4f77fb..418fab58a 100644 --- a/app/graph/types/team_type.rb +++ b/app/graph/types/team_type.rb @@ -380,6 +380,7 @@ def api_key(dbid:) end field :api_keys, ApiKeyType.connection_type, null: true + def api_keys ability = context[:ability] || Ability.new api_keys = object.api_keys.order(created_at: :desc) @@ -388,4 +389,15 @@ def api_keys ability.can?(:read, api_key) end end + + field :statistics, TeamStatisticsType, null: true do + argument :period, GraphQL::Types::String, required: true # FIXME: List/validate possible values + argument :language, GraphQL::Types::String, required: false + argument :platform, GraphQL::Types::String, required: false # FIXME: List/validate possible values + end + + def statistics(period:, language: nil, platform: nil) + # FIXME: Check for permissions + TeamStatistics.new(object, period, language, platform) + end end diff --git a/app/models/tipline_request.rb b/app/models/tipline_request.rb index 62e5bf28d..3b753a78d 100644 --- a/app/models/tipline_request.rb +++ b/app/models/tipline_request.rb @@ -2,6 +2,7 @@ class TiplineRequest < ApplicationRecord include CheckElasticSearch belongs_to :associated, polymorphic: true + belongs_to :project_media, -> { where(tipline_requests: { associated_type: 'ProjectMedia' }) }, foreign_key: 'associated_id', optional: true belongs_to :user, optional: true before_validation :set_team_and_user, :set_smooch_data_fields, on: :create diff --git a/db/migrate/20241015223059_add_index_to_created_at_for_articles.rb b/db/migrate/20241015223059_add_index_to_created_at_for_articles.rb new file mode 100644 index 000000000..cc06680c6 --- /dev/null +++ b/db/migrate/20241015223059_add_index_to_created_at_for_articles.rb @@ -0,0 +1,8 @@ +class AddIndexToCreatedAtForArticles < ActiveRecord::Migration[6.1] + def change + execute "CREATE INDEX fact_check_created_at_day ON fact_checks (date_trunc('day', created_at))" + add_index :fact_checks, :created_at + execute "CREATE INDEX explainer_created_at_day ON explainers (date_trunc('day', created_at))" + add_index :explainers, :created_at + end +end diff --git a/db/schema.rb b/db/schema.rb index bccdebba3..dfb106d58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_10_09_192811) do +ActiveRecord::Schema.define(version: 2024_10_15_223059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -292,7 +292,7 @@ t.jsonb "value_json", default: "{}" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index "dynamic_annotation_fields_value(field_name, value)", name: "dynamic_annotation_fields_value", where: "((field_name)::text = ANY (ARRAY[('external_id'::character varying)::text, ('smooch_user_id'::character varying)::text, ('verification_status_status'::character varying)::text]))" + t.index "dynamic_annotation_fields_value(field_name, value)", name: "dynamic_annotation_fields_value", where: "((field_name)::text = ANY ((ARRAY['external_id'::character varying, 'smooch_user_id'::character varying, 'verification_status_status'::character varying])::text[]))" t.index ["annotation_id", "field_name"], name: "index_dynamic_annotation_fields_on_annotation_id_and_field_name" t.index ["annotation_id"], name: "index_dynamic_annotation_fields_on_annotation_id" t.index ["annotation_type"], name: "index_dynamic_annotation_fields_on_annotation_type" @@ -326,6 +326,8 @@ t.datetime "updated_at", precision: 6, null: false t.string "tags", default: [], array: true t.boolean "trashed", default: false + t.index "date_trunc('day'::text, created_at)", name: "explainer_created_at_day" + t.index ["created_at"], name: "index_explainers_on_created_at" t.index ["tags"], name: "index_explainers_on_tags", using: :gin t.index ["team_id"], name: "index_explainers_on_team_id" t.index ["user_id"], name: "index_explainers_on_user_id" @@ -347,7 +349,9 @@ t.string "rating" t.boolean "imported", default: false t.boolean "trashed", default: false + t.index "date_trunc('day'::text, created_at)", name: "fact_check_created_at_day" t.index ["claim_description_id"], name: "index_fact_checks_on_claim_description_id", unique: true + t.index ["created_at"], name: "index_fact_checks_on_created_at" t.index ["imported"], name: "index_fact_checks_on_imported" t.index ["language"], name: "index_fact_checks_on_language" t.index ["publisher_id"], name: "index_fact_checks_on_publisher_id" diff --git a/lib/check_data_points.rb b/lib/check_data_points.rb index 160056f0a..744312abc 100644 --- a/lib/check_data_points.rb +++ b/lib/check_data_points.rb @@ -4,44 +4,47 @@ class << self GRANULARITY_VALUES = ['year', 'quarter', 'month', 'week', 'day'] # Number of tipline messages - def tipline_messages(team_id, start_date, end_date, granularity = nil) + def tipline_messages(team_id, start_date, end_date, granularity = nil, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) query = TiplineMessage.where(team_id: team_id, created_at: start_date..end_date) - query_based_on_granularity(query, granularity) + query_based_on_granularity(query, platform, language, granularity) end # Number of tipline requests - def tipline_requests(team_id, start_date, end_date, granularity = nil) + def tipline_requests(team_id, start_date, end_date, granularity = nil, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) query = TiplineRequest.where(team_id: team_id, created_at: start_date..end_date) - query_based_on_granularity(query, granularity) + query_based_on_granularity(query, platform, language, granularity) end # Number of tipline requests grouped by type of search result - def tipline_requests_by_search_type(team_id, start_date, end_date) + def tipline_requests_by_search_type(team_id, start_date, end_date, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) - TiplineRequest.where( + query = TiplineRequest.where( team_id: team_id, smooch_request_type: SEARCH_RESULT_TYPES, created_at: start_date..end_date, - ).group('smooch_request_type').count + ) + query = query.where(platform: platform) unless platform.blank? + query = query.where(language: language) unless language.blank? + query.group('smooch_request_type').count end # Number of Subscribers - def tipline_subscriptions(team_id, start_date, end_date, granularity = nil) + def tipline_subscriptions(team_id, start_date, end_date, granularity = nil, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) query = TiplineSubscription.where(team_id: team_id, created_at: start_date..end_date) - query_based_on_granularity(query, granularity) + query_based_on_granularity(query, platform, language, granularity) end # Number of Newsletters sent - def newsletters_sent(team_id, start_date, end_date, granularity = nil) + def newsletters_sent(team_id, start_date, end_date, granularity = nil, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) query = TiplineNewsletterDelivery .joins(:tipline_newsletter) .where('tipline_newsletters.team_id': team_id) .where(created_at: start_date..end_date) - query_based_on_granularity(query, granularity, 'newsletter') + query_based_on_granularity(query, platform, language, granularity, 'newsletter') end # Number of Media received, by type @@ -53,48 +56,62 @@ def media_received_by_type(team_id, start_date, end_date) end # Top clusters - def top_clusters(team_id, start_date, end_date, limit = 5) - elastic_search_top_items(team_id, start_date, end_date, limit) + def top_clusters(team_id, start_date, end_date, limit = 5, range_field = 'created_at', language = nil, language_field = 'language', platform = nil) + elastic_search_top_items(team_id, start_date, end_date, limit, false, range_field, language, language_field, platform) end # Top media tags - def top_media_tags(team_id, start_date, end_date, limit = 5) - elastic_search_top_items(team_id, start_date, end_date, limit, true) + def top_media_tags(team_id, start_date, end_date, limit = 5, range_field = 'created_at', language = nil, language_field = 'language', platform = nil) + elastic_search_top_items(team_id, start_date, end_date, limit, true, range_field, language, language_field, platform) end # Articles sent - def articles_sent(team_id, start_date, end_date) + def articles_sent(team_id, start_date, end_date, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) # Get number of articles sent as search results - search_result_c = TiplineRequest.where(team_id: team_id, smooch_request_type: SEARCH_RESULT_TYPES, created_at: start_date..end_date).count + search_results_query = TiplineRequest.where(team_id: team_id, smooch_request_type: SEARCH_RESULT_TYPES, created_at: start_date..end_date) + search_results_query = search_results_query.where(platform: platform) unless platform.blank? + search_results_query = search_results_query.where(language: language) unless language.blank? + search_results_count = search_results_query.count # Get the number of articles sent as reports - reports_c = TiplineRequest + reports_query = TiplineRequest .where(team_id: team_id, created_at: start_date..end_date) - .where('smooch_report_received_at > 0 OR smooch_report_update_received_at > 0 OR smooch_report_sent_at > 0 OR smooch_report_correction_sent_at > 0').count - search_result_c + reports_c + .where('smooch_report_received_at > 0 OR smooch_report_update_received_at > 0 OR smooch_report_sent_at > 0 OR smooch_report_correction_sent_at > 0') + reports_query = reports_query.where(platform: platform) unless platform.blank? + reports_query = reports_query.where(language: language) unless language.blank? + reports_count = reports_query.count + search_results_count + reports_count end # Average response time - def average_response_time(team_id, start_date, end_date) - TiplineRequest - .where(team_id: team_id, smooch_report_received_at: start_date.to_datetime.to_i..end_date.to_datetime.to_i) - .average("smooch_report_received_at - CAST(DATE_PART('EPOCH', created_at::timestamp) AS INTEGER)").to_f + def average_response_time(team_id, start_date, end_date, platform = nil, language = nil) + query = TiplineRequest.where(team_id: team_id, smooch_report_received_at: start_date.to_datetime.to_i..end_date.to_datetime.to_i) + query = query.where(platform: platform) unless platform.blank? + query = query.where(language: language) unless language.blank? + query.average("smooch_report_received_at - CAST(DATE_PART('EPOCH', created_at::timestamp) AS INTEGER)").to_f end # All users - def all_users(team_id, start_date, end_date) + def all_users(team_id, start_date, end_date, platform = nil, language = nil) start_date, end_date = parse_start_end_dates(start_date, end_date) - TiplineRequest.where(team_id: team_id, created_at: start_date..end_date) - .count('DISTINCT(tipline_user_uid)') + query = TiplineRequest.where(team_id: team_id, created_at: start_date..end_date) + query = query.where(platform: platform) unless platform.blank? + query = query.where(language: language) unless language.blank? + query.count('DISTINCT(tipline_user_uid)') end # Returning users - def returning_users(team_id, start_date, end_date) + def returning_users(team_id, start_date, end_date, platform = nil, language = nil) # Number of returning users (at least one session in the current month, and at least one session in the last previous 2 months) start_date, end_date = parse_start_end_dates(start_date, end_date) - uids = TiplineRequest.where(team_id: team_id, created_at: start_date.ago(2.months)..start_date).map(&:tipline_user_uid).uniq - TiplineRequest.where(team_id: team_id, tipline_user_uid: uids, created_at: start_date..end_date) - .count('DISTINCT(tipline_user_uid)') + uids_query = TiplineRequest.where(team_id: team_id, created_at: start_date.ago(2.months)..start_date) + uids_query = uids_query.where(platform: platform) unless platform.blank? + uids_query = uids_query.where(language: language) unless language.blank? + uids = uids_query.select(:tipline_user_uid).map(&:tipline_user_uid).uniq + query = TiplineRequest.where(team_id: team_id, tipline_user_uid: uids, created_at: start_date..end_date) + query = query.where(platform: platform) unless platform.blank? + query = query.where(language: language) unless language.blank? + query.count('DISTINCT(tipline_user_uid)') end # New users @@ -116,7 +133,9 @@ def parse_start_end_dates(start_date, end_date) return start_date, end_date end - def query_based_on_granularity(query, granularity, type = nil) + def query_based_on_granularity(query, platform, language, granularity, type = nil) + query = query.where(platform: platform) unless platform.blank? + query = query.where(language: language) unless language.blank? # For PG the allowed values for granularity can be one of the following # [millennium, century, decade, year, quarter, month, week, day, hour, # minute, second, milliseconds, microseconds] @@ -132,15 +151,17 @@ def query_based_on_granularity(query, granularity, type = nil) end end - def elastic_search_top_items(team_id, start_date, end_date, limit, with_tags = false) + def elastic_search_top_items(team_id, start_date, end_date, limit, with_tags = false, range_field = 'created_at', language = nil, language_field = 'language', platform = nil) data = {} query = { - range: { 'created_at': { start_time: start_date, end_time: end_date } }, + range: { range_field => { start_time: start_date, end_time: end_date } }, demand: { min: 1 }, sort: 'demand', eslimit: limit } query[:tags_as_sentence] = { min: 1 } if with_tags + query[language_field.to_sym] = [language].flatten if language + query[:channels] = [CheckChannels::ChannelCodes.all_channels['TIPLINE'][platform.upcase]] if platform result = CheckSearch.new(query.to_json, nil, team_id) result.medias.each{ |pm| data[pm.id] = pm.demand } data diff --git a/lib/relay.idl b/lib/relay.idl index bee36de83..b672574b3 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -13372,6 +13372,7 @@ type Team implements Node { ): SourceConnection sources_count(keyword: String): Int spam_count: Int + statistics(language: String, period: String!, platform: String): TeamStatistics tag_texts( """ Returns the elements in the list that come after the specified cursor. @@ -13688,6 +13689,42 @@ type TeamEdge { node: Team } +""" +Workspace statistics. +""" +type TeamStatistics implements Node { + average_response_time: Int + created_at: String + id: ID! + number_of_articles_created_by_date: JsonStringType + number_of_articles_sent: Int + number_of_articles_updated_by_date: JsonStringType + number_of_conversations: Int + number_of_conversations_by_date: JsonStringType + number_of_explainers_created: Int + number_of_fact_checks_by_rating: JsonStringType + number_of_fact_checks_created: Int + number_of_matched_results_by_article_type: JsonStringType + number_of_media_received_by_media_type: JsonStringType + number_of_messages: Int + number_of_messages_by_date: JsonStringType + number_of_new_subscribers: Int + number_of_newsletters_delivered: Int + number_of_newsletters_sent: Int + number_of_published_fact_checks: Int + number_of_returning_users: Int + number_of_search_results_by_feedback_type: JsonStringType + number_of_subscribers: Int + number_of_total_users: Int + number_of_unique_users: Int + permissions: String + top_articles_sent: JsonStringType + top_articles_tags: JsonStringType + top_media_tags: JsonStringType + top_requested_media_clusters: JsonStringType + updated_at: String +} + """ Team task type """ diff --git a/lib/team_statistics.rb b/lib/team_statistics.rb new file mode 100644 index 000000000..e8178b828 --- /dev/null +++ b/lib/team_statistics.rb @@ -0,0 +1,302 @@ +class TeamStatistics + PERIODS = ['past_week', 'past_2_weeks', 'past_month', 'past_3_months', 'past_6_months', 'year_to_date'] + + PLATFORMS = Bot::Smooch::SUPPORTED_INTEGRATION_NAMES + + def initialize(team, period, language, platform = nil) + @team = team + unless @team.is_a?(Team) + raise ArgumentError.new('Invalid workspace provided') + end + + @period = period + unless PERIODS.include?(@period) + raise ArgumentError.new("Invalid period provided. Allowed values: #{PERIODS.join(', ')}") + end + + range = time_range.to_a + @start_date, @end_date = range.first, range.last + @start_date_str, @end_date_str = @start_date.strftime('%Y-%m-%d'), @end_date.strftime('%Y-%m-%d') + + @platform = platform + if !@platform.blank? && !PLATFORMS.keys.include?(@platform) + # For `Bot::Smooch::SUPPORTED_INTEGRATION_NAMES`, the keys (e.g., 'whatsapp') are used by `TiplineRequest`, + # while the values (e.g., 'WhatsApp') are used by `TiplineMessage` + raise ArgumentError.new("Invalid platform provided. Allowed values: #{PLATFORMS.keys.join(', ')}") + end + @platform_name = PLATFORMS[@platform] unless @platform.blank? + + @language = language + @all_languages = [@team.get_languages.to_a, 'und'].flatten + end + + # For GraphQL + def id + Base64.encode64("TeamStatistics/#{@team.id}") + end + + # For articles + + def number_of_articles_created_by_date + number_of_articles_saved_by_date(:created_at) + end + + def number_of_articles_updated_by_date + number_of_articles_saved_by_date(:updated_at) + end + + def number_of_explainers_created + explainers_base_query.count + end + + def number_of_fact_checks_created + fact_checks_base_query.count + end + + def number_of_published_fact_checks + fact_checks_base_query.where(report_status: 'published').count + end + + def number_of_fact_checks_by_rating + fact_checks_base_query.group(:rating).count.sort.to_h + end + + # FIXME: Only fact-checks for now (need to add explainers) and the "demand" is across languages and platforms + def top_articles_sent + data = {} + clusters = CheckDataPoints.top_clusters(@team.id, @start_date, @end_date, 5, 'last_seen', @language || @all_languages, 'fc_language') + clusters.each do |pm_id, demand| + item = ProjectMedia.find(pm_id) + data[item.fact_check_title || item.title] = demand + end + data + end + + def top_articles_tags + sql = <<-SQL + SELECT tag, COUNT(*) as tag_count + FROM ( + SELECT unnest(fcs.tags) AS tag FROM fact_checks fcs + INNER JOIN claim_descriptions cds ON fcs.claim_description_id = cds.id + WHERE cds.team_id = :team_id AND fcs.created_at BETWEEN :start_date AND :end_date AND fcs.language IN (:language) + UNION ALL + SELECT unnest(explainers.tags) AS tag FROM explainers + WHERE explainers.team_id = :team_id AND explainers.created_at BETWEEN :start_date AND :end_date AND explainers.language IN (:language) + ) AS all_tags + GROUP BY tag + ORDER BY tag_count DESC + LIMIT 5 + SQL + + language = @language ? [@language] : @all_languages + result = ActiveRecord::Base.connection.execute(ApplicationRecord.sanitize_sql_for_assignment([sql, team_id: @team.id, start_date: @start_date, end_date: @end_date, language: language])) + data = {} + result.each do |row| + data[row['tag']] = row['tag_count'].to_i + end + data.sort.reverse.to_h + end + + # For tiplines + + def number_of_messages + CheckDataPoints.tipline_messages(@team.id, @start_date_str, @end_date_str, nil, @platform_name, @language) + end + + def number_of_conversations + CheckDataPoints.tipline_requests(@team.id, @start_date_str, @end_date_str, nil, @platform, @language) + end + + def number_of_messages_by_date + data = CheckDataPoints.tipline_messages(@team.id, @start_date_str, @end_date_str, 'day', @platform_name, @language) + number_of_tipline_data_points_by_date(data) + end + + def number_of_conversations_by_date + data = CheckDataPoints.tipline_requests(@team.id, @start_date_str, @end_date_str, 'day', @platform, @language) + number_of_tipline_data_points_by_date(data) + end + + def number_of_search_results_by_feedback_type + mapping = { + relevant_search_result_requests: 'Positive', + irrelevant_search_result_requests: 'Negative', + timeout_search_requests: 'No Response' + } + data = { + 'Positive' => 0, + 'Negative' => 0, + 'No Response' => 0 + } + CheckDataPoints.tipline_requests_by_search_type(@team.id, @start_date_str, @end_date_str, @platform, @language).each do |type, count| + data[mapping[type.to_sym]] = count + end + data + end + + def average_response_time + CheckDataPoints.average_response_time(@team.id, @start_date, @end_date, @platform, @language) + end + + def number_of_unique_users + number_of_total_users - number_of_returning_users + end + + def number_of_total_users + CheckDataPoints.all_users(@team.id, @start_date_str, @end_date_str, @platform, @language) + end + + def number_of_returning_users + CheckDataPoints.returning_users(@team.id, @start_date_str, @end_date_str, @platform, @language) + end + + def number_of_subscribers + CheckDataPoints.tipline_subscriptions(@team.id, @team.created_at.strftime('%Y-%m-%d'), @end_date_str, nil, @platform_name, @language) + end + + def number_of_new_subscribers + CheckDataPoints.tipline_subscriptions(@team.id, @start_date_str, @end_date_str, nil, @platform_name, @language) + end + + def number_of_newsletters_sent + number_of_newsletters('sent') + end + + def number_of_newsletters_delivered + number_of_newsletters('delivered') + end + + def number_of_media_received_by_media_type + conditions = { team_id: @team.id, created_at: @start_date..@end_date } + conditions[:language] = @language unless @language.blank? + conditions[:platform] = @platform unless @platform.blank? + data = TiplineRequest + .joins("INNER JOIN project_medias pm ON tipline_requests.associated_type = 'ProjectMedia' AND pm.id = tipline_requests.associated_id") + .joins("INNER JOIN medias m ON m.id = pm.media_id") + .where(conditions) + .group('m.type') + .count + { 'Claim' => 0, 'Link' => 0, 'UploadedAudio' => 0, 'UploadedImage' => 0, 'UploadedVideo' => 0 }.merge(data).reject{ |k, _v| k == 'Blank' } + end + + # FIXME: The "demand" is across languages and platforms + def top_requested_media_clusters + data = {} + clusters = CheckDataPoints.top_clusters(@team.id, @start_date, @end_date, 5, 'last_seen', @language || @all_languages, 'request_language', @platform) + clusters.each do |pm_id, demand| + item = ProjectMedia.find(pm_id) + data[item.title] = demand + end + data + end + + # FIXME: The "demand" is across languages and platforms + def top_media_tags + data = {} + clusters = CheckDataPoints.top_clusters(@team.id, @start_date, @end_date, 5, 'last_seen', @language || @all_languages, 'language', @platform) + clusters.each do |pm_id, demand| + item = ProjectMedia.find(pm_id) + item.tags_as_sentence.split(',').map(&:strip).each do |tag| + data[tag] ||= 0 + data[tag] += demand + end + end + data.sort_by{ |_key, value| value }.reverse.first(5).to_h + end + + # For both articles and tiplines + + def number_of_articles_sent + CheckDataPoints.articles_sent(@team.id, @start_date_str, @end_date_str, @platform, @language) + end + + def number_of_matched_results_by_article_type + query = TiplineRequest.where(team_id: @team.id, smooch_request_type: ['relevant_search_result_requests', 'irrelevant_search_result_requests', 'timeout_search_requests'], created_at: @start_date..@end_date) + query = query.where(platform: @platform) unless @platform.blank? + query = query.where(language: @language) unless @language.blank? + { 'FactCheck' => query.joins(project_media: { claim_description: :fact_check }).count, 'Explainer' => query.joins(project_media: :explainers).count } + end + + private + + def time_range + ago = { + past_week: 1.week, + past_2_weeks: 2.weeks, + past_month: 1.month, + past_3_months: 3.months, + past_6_months: 6.months + }[@period.to_sym] + from = Time.now.ago(ago) unless ago.nil? + from = Time.now.beginning_of_year if @period.to_s == 'year_to_date' + from.to_datetime..Time.now.to_datetime + end + + def fact_checks_base_query(timestamp_field = :created_at, group_by_day = false) + query = FactCheck.joins(:claim_description).where(timestamp_field => time_range, 'claim_descriptions.team_id' => @team.id) + query = query.where('fact_checks.created_at != fact_checks.updated_at') if timestamp_field.to_sym == :updated_at + query = query.where(language: @language) unless @language.blank? + if group_by_day + # Avoid SQL injection warning + group = { + created_at: "date_trunc('day', fact_checks.created_at)", + updated_at: "date_trunc('day', fact_checks.updated_at)" + }[timestamp_field.to_sym] + query = query.group(group) + end + query + end + + def explainers_base_query(timestamp_field = :created_at, group_by_day = false) + query = Explainer.where(timestamp_field => time_range, 'team_id' => @team.id) + query = query.where(language: @language) unless @language.blank? + query = query.where('explainers.created_at != explainers.updated_at') if timestamp_field.to_sym == :updated_at + if group_by_day + # Avoid SQL injection warning + group = { + created_at: "date_trunc('day', explainers.created_at)", + updated_at: "date_trunc('day', explainers.updated_at)" + }[timestamp_field.to_sym] + query = query.group(group) + end + query + end + + def number_of_articles_saved_by_date(timestamp_field) # timestamp_field = :created_at or :updated_at + raise ArgumentError if timestamp_field != :created_at && timestamp_field != :updated_at + number_of_fact_checks = fact_checks_base_query(timestamp_field, true).count + number_of_explainers = explainers_base_query(timestamp_field, true).count + number_of_articles = {} + + # Pre-fill with zeros + time_range.to_a.each do |day| + number_of_articles[day.strftime("%Y-%m-%d")] = 0 + end + + # Replace zeros by the days we have data for + (number_of_fact_checks.keys + number_of_explainers.keys).uniq.sort.each do |day| + number_of_articles[day.strftime("%Y-%m-%d")] = number_of_fact_checks[day].to_i + number_of_explainers[day].to_i + end + + number_of_articles + end + + def number_of_tipline_data_points_by_date(results) + data = {} + # Pre-fill with zeros + time_range.to_a.each do |day| + data[day.strftime("%Y-%m-%d")] = 0 + end + results.each do |day, count| + data[day.strftime("%Y-%m-%d")] = count + end + data + end + + def number_of_newsletters(state) + query = TiplineMessage.where(created_at: @start_date..@end_date, team_id: @team.id, state: state, event: 'newsletter') + query = query.where(language: @language) unless @language.blank? + query = query.where(platform: @platform_name) unless @platform.blank? + query.count + end +end diff --git a/public/relay.json b/public/relay.json index 2d4750a1b..2fc923085 100644 --- a/public/relay.json +++ b/public/relay.json @@ -56461,6 +56461,11 @@ "name": "TeamBotInstallation", "ofType": null }, + { + "kind": "OBJECT", + "name": "TeamStatistics", + "ofType": null + }, { "kind": "OBJECT", "name": "TeamTask", @@ -70508,6 +70513,59 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "statistics", + "description": null, + "args": [ + { + "name": "period", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "language", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "platform", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TeamStatistics", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "tag_texts", "description": null, @@ -71877,6 +71935,447 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "TeamStatistics", + "description": "Workspace statistics.", + "fields": [ + { + "name": "average_response_time", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_articles_created_by_date", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_articles_sent", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_articles_updated_by_date", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_conversations", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_conversations_by_date", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_explainers_created", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_fact_checks_by_rating", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_fact_checks_created", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_matched_results_by_article_type", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_media_received_by_media_type", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_messages", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_messages_by_date", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_new_subscribers", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_newsletters_delivered", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_newsletters_sent", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_published_fact_checks", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_returning_users", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_search_results_by_feedback_type", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_subscribers", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_total_users", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number_of_unique_users", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "top_articles_sent", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "top_articles_tags", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "top_media_tags", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "top_requested_media_clusters", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "JsonStringType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "TeamTask", diff --git a/test/controllers/graphql_controller_11_test.rb b/test/controllers/graphql_controller_11_test.rb index ed47c24ba..08eb5ded8 100644 --- a/test/controllers/graphql_controller_11_test.rb +++ b/test/controllers/graphql_controller_11_test.rb @@ -201,6 +201,51 @@ def teardown end end + test "should get team statistics" do + user = create_user + team = create_team + create_team_user user: user, team: team, role: 'admin' + + authenticate_with_user(user) + query = <<~GRAPHQL + query { + team(slug: "#{team.slug}") { + statistics(period: "past_week", platform: "whatsapp", language: "en") { + number_of_articles_created_by_date + number_of_articles_updated_by_date + number_of_explainers_created + number_of_fact_checks_created + number_of_published_fact_checks + number_of_fact_checks_by_rating + top_articles_sent + top_articles_tags + number_of_messages + number_of_conversations + number_of_messages_by_date + number_of_conversations_by_date + number_of_search_results_by_feedback_type + average_response_time + number_of_unique_users + number_of_total_users + number_of_returning_users + number_of_subscribers + number_of_new_subscribers + number_of_newsletters_sent + number_of_newsletters_delivered + top_media_tags + top_requested_media_clusters + number_of_media_received_by_media_type + number_of_articles_sent + number_of_matched_results_by_article_type + } + } + } + GRAPHQL + + post :create, params: { query: query } + assert_response :success + end + test "should not get requests if interval is more than one month" do u = create_user t = create_team @@ -222,6 +267,7 @@ def teardown } } GRAPHQL + post :create, params: { query: query, team: t.slug } assert_response 400 assert_equal 'Maximum interval is one month.', JSON.parse(@response.body)['errors'][0]['message'] @@ -250,6 +296,7 @@ def teardown } } GRAPHQL + post :create, params: { query: query, team: t.slug } assert_response :success assert_equal 2, JSON.parse(@response.body).dig('data', 'team', 'tipline_requests', 'edges').size diff --git a/test/lib/team_statistics_test.rb b/test/lib/team_statistics_test.rb new file mode 100644 index 000000000..8e0b51dfe --- /dev/null +++ b/test/lib/team_statistics_test.rb @@ -0,0 +1,211 @@ +require_relative '../test_helper' + +class TeamStatisticsTest < ActiveSupport::TestCase + def setup + @team = create_team + @team.set_languages = ['en', 'pt'] + @team.save! + end + + def teardown + end + + test "should provide a valid period" do + assert_raises ArgumentError do + TeamStatistics.new(@team, 'past_century', 'en', 'whatsapp') + end + + assert_nothing_raised do + TeamStatistics.new(@team, 'past_month', 'en', 'whatsapp') + end + end + + test "should provide a valid workspace" do + assert_raises ArgumentError do + TeamStatistics.new(Class.new, 'past_month', 'en', 'whatsapp') + end + + assert_nothing_raised do + TeamStatistics.new(@team, 'past_month', 'en', 'whatsapp') + end + end + + test "should provide a valid platform" do + assert_raises ArgumentError do + TeamStatistics.new(@team, 'past_month', 'en', 'icq') + end + + assert_nothing_raised do + TeamStatistics.new(@team, 'past_month', 'en', 'whatsapp') + end + end + + test "should have a GraphQL ID" do + assert_kind_of String, TeamStatistics.new(@team, 'past_month', 'en', 'whatsapp').id + end + + test "should return articles statistics" do + team = create_team + exp = nil + + travel_to Time.parse('2024-01-01') do + create_fact_check(tags: ['foo', 'bar'], language: 'en', rating: 'false', claim_description: create_claim_description(project_media: create_project_media(team: @team))) + create_fact_check(tags: ['foo', 'bar'], claim_description: create_claim_description(project_media: create_project_media(team: team))) + exp = create_explainer team: @team, language: 'en', tags: ['foo'] + create_explainer team: @team, tags: ['foo', 'bar'] + create_explainer language: 'en', team: team, tags: ['foo', 'bar'] + end + + travel_to Time.parse('2024-01-02') do + create_fact_check(tags: ['bar'], report_status: 'published', rating: 'verified', language: 'en', claim_description: create_claim_description(project_media: create_project_media(team: @team))) + create_fact_check(tags: ['foo', 'bar'], claim_description: create_claim_description(project_media: create_project_media(team: team))) + create_explainer team: @team, language: 'en', tags: ['foo'] + create_explainer team: @team, tags: ['foo', 'bar'] + create_explainer language: 'en', team: team, tags: ['foo', 'bar'] + exp.updated_at = Time.now + exp.save! + end + + travel_to Time.parse('2024-01-08') do + object = TeamStatistics.new(@team, 'past_week', 'en') + assert_equal({ '2024-01-01' => 2, '2024-01-02' => 2, '2024-01-03' => 0, '2024-01-04' => 0, '2024-01-05' => 0, '2024-01-06' => 0, '2024-01-07' => 0, '2024-01-08' => 0 }, + object.number_of_articles_created_by_date) + assert_equal({ '2024-01-01' => 0, '2024-01-02' => 1, '2024-01-03' => 0, '2024-01-04' => 0, '2024-01-05' => 0, '2024-01-06' => 0, '2024-01-07' => 0, '2024-01-08' => 0 }, + object.number_of_articles_updated_by_date) + assert_equal 2, object.number_of_explainers_created + assert_equal 2, object.number_of_fact_checks_created + assert_equal 1, object.number_of_published_fact_checks + assert_equal({ 'false' => 1, 'verified' => 1 }, object.number_of_fact_checks_by_rating) + assert_equal({ 'foo' => 3, 'bar' => 2 }, object.top_articles_tags) + end + end + + test "should return number of articles sent" do + setup_elasticsearch + RequestStore.store[:skip_cached_field_update] = false + + pm1 = create_project_media team: @team, disable_es_callbacks: false + create_fact_check title: 'Bar', report_status: 'published', rating: 'verified', language: 'en', claim_description: create_claim_description(project_media: pm1), disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm1 + + pm2 = create_project_media team: @team, disable_es_callbacks: false + create_fact_check title: 'Foo', report_status: 'published', rating: 'verified', language: 'en', claim_description: create_claim_description(project_media: pm2), disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm2 + create_tipline_request team_id: @team.id, associated: pm2 + + sleep 2 + + object = TeamStatistics.new(@team, 'past_week', 'en') + expected = { 'Foo' => 2, 'Bar' => 1 } + assert_equal expected, object.top_articles_sent + end + + test "should return tipline statistics" do + pm1 = create_project_media team: @team, quote: 'Test' + create_fact_check claim_description: create_claim_description(project_media: pm1) + exp = create_explainer team: @team + pm1.explainers << exp + team = create_team + pm2 = create_project_media team: team + + travel_to Time.parse('2024-01-01') do + 2.times { create_tipline_message team_id: @team.id, language: 'en', platform: 'WhatsApp' } + create_tipline_message team_id: @team.id, language: 'en', platform: 'Telegram' + create_tipline_message team_id: @team.id, language: 'pt', platform: 'WhatsApp' + create_tipline_message team_id: team.id, language: 'en', platform: 'WhatsApp' + + create_tipline_request team_id: @team.id, associated: pm1, language: 'en', platform: 'whatsapp', smooch_request_type: 'relevant_search_result_requests' + create_tipline_request team_id: team.id, associated: pm2, language: 'en', platform: 'whatsapp', smooch_request_type: 'relevant_search_result_requests' + create_tipline_request team_id: @team.id, associated: pm1, language: 'pt', platform: 'whatsapp', smooch_request_type: 'relevant_search_result_requests' + create_tipline_request team_id: @team.id, associated: pm1, language: 'en', platform: 'telegram', smooch_request_type: 'relevant_search_result_requests' + end + + travel_to Time.parse('2024-01-03') do + 3.times { create_tipline_message team_id: @team.id, language: 'en', platform: 'WhatsApp' } + create_tipline_message team_id: @team.id, language: 'en', platform: 'Telegram' + create_tipline_message team_id: @team.id, language: 'pt', platform: 'WhatsApp' + create_tipline_message team_id: team.id, language: 'en', platform: 'WhatsApp' + + 2.times { create_tipline_request team_id: @team.id, associated: pm1, language: 'en', platform: 'whatsapp', smooch_request_type: 'irrelevant_search_result_requests' } + create_tipline_request team_id: team.id, associated: pm2, language: 'en', platform: 'whatsapp', smooch_request_type: 'relevant_search_result_requests' + create_tipline_request team_id: @team.id, associated: pm1, language: 'pt', platform: 'whatsapp', smooch_request_type: 'relevant_search_result_requests' + create_tipline_request team_id: @team.id, associated: pm1, language: 'en', platform: 'telegram', smooch_request_type: 'relevant_search_result_requests' + end + + travel_to Time.parse('2024-01-08') do + object = TeamStatistics.new(@team, 'past_week', 'en', 'whatsapp') + assert_equal 5, object.number_of_messages + assert_equal({ '2024-01-01' => 2, '2024-01-02' => 0, '2024-01-03' => 3, '2024-01-04' => 0, '2024-01-05' => 0, '2024-01-06' => 0, '2024-01-07' => 0, '2024-01-08' => 0 }, + object.number_of_messages_by_date) + assert_equal 3, object.number_of_conversations + assert_equal({ '2024-01-01' => 1, '2024-01-02' => 0, '2024-01-03' => 2, '2024-01-04' => 0, '2024-01-05' => 0, '2024-01-06' => 0, '2024-01-07' => 0, '2024-01-08' => 0 }, + object.number_of_conversations_by_date) + assert_equal({ 'Positive' => 1, 'Negative' => 2, 'No Response' => 0 }, object.number_of_search_results_by_feedback_type) + assert_equal({ 'Claim' => 3, 'Link' => 0, 'UploadedAudio' => 0, 'UploadedImage' => 0, 'UploadedVideo' => 0 }, object.number_of_media_received_by_media_type) + assert_equal 3, object.number_of_articles_sent + assert_equal({ 'FactCheck' => 3, 'Explainer' => 3 }, object.number_of_matched_results_by_article_type) + end + end + + test "should return top requested media clusters" do + setup_elasticsearch + RequestStore.store[:skip_cached_field_update] = false + channel = CheckChannels::ChannelCodes::WHATSAPP + Sidekiq::Testing.inline! do + pm1 = create_project_media team: @team, quote: 'Bar', channel: { main: channel, others: [channel] }, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm1, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm2 = create_project_media team: @team, quote: 'Foo', channel: { main: channel, others: [channel] }, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm2, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm2, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm3 = create_project_media team: @team, quote: 'Test 1', channel: { main: 0, others: [0] }, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm3, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm4 = create_project_media team: @team, quote: 'Test 2', channel: { main: channel, others: [channel] }, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm4, platform: 'whatsapp', language: 'pt', disable_es_callbacks: false + + sleep 3 + + object = TeamStatistics.new(@team, 'past_week', 'en', 'whatsapp') + expected = { 'Foo' => 2, 'Bar' => 1 } + assert_equal expected, object.top_requested_media_clusters + end + end + + test "should return top media tags" do + setup_elasticsearch + RequestStore.store[:skip_cached_field_update] = false + channel = CheckChannels::ChannelCodes::WHATSAPP + TestDynamicAnnotationTables.load! + create_annotation_type_and_fields('Language', { 'Language' => ['Text', true] }) + Sidekiq::Testing.inline! do + pm1 = create_project_media team: @team, channel: { main: channel, others: [channel] }, tags: ['foo', 'bar'], disable_es_callbacks: false + create_dynamic_annotation annotation_type: 'language', annotated: pm1, set_fields: { language: 'en' }.to_json, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm1, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm2 = create_project_media team: @team, channel: { main: channel, others: [channel] }, tags: ['foo', 'test'], disable_es_callbacks: false + create_dynamic_annotation annotation_type: 'language', annotated: pm2, set_fields: { language: 'en' }.to_json, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm2, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm2, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm3 = create_project_media team: @team, channel: { main: 0, others: [0] }, tags: ['test-1'], disable_es_callbacks: false + create_dynamic_annotation annotation_type: 'language', annotated: pm3, set_fields: { language: 'en' }.to_json, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm3, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + pm4 = create_project_media team: @team, channel: { main: channel, others: [channel] }, tags: ['test-2'], disable_es_callbacks: false + create_dynamic_annotation annotation_type: 'language', annotated: pm4, set_fields: { language: 'pt' }.to_json, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm4, platform: 'whatsapp', language: 'pt', disable_es_callbacks: false + + pm5 = create_project_media team: @team, channel: { main: channel, others: [channel] }, disable_es_callbacks: false + create_dynamic_annotation annotation_type: 'language', annotated: pm5, set_fields: { language: 'en' }.to_json, disable_es_callbacks: false + create_tipline_request team_id: @team.id, associated: pm4, platform: 'whatsapp', language: 'en', disable_es_callbacks: false + + sleep 3 + + object = TeamStatistics.new(@team, 'past_week', 'en', 'whatsapp') + expected = { 'foo' => 3, 'test' => 2, 'bar' => 1 } + assert_equal expected, object.top_media_tags + end + end +end