diff --git a/app/models/report.rb b/app/models/report.rb index 9a5726e6c72..c866d73f280 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -214,48 +214,6 @@ class Report report end - def self.report_consolidated_page_views(report) - filters = %w[ - page_view_logged_in - page_view_anon - page_view_crawler - ] - - report.modes = [:stacked_chart] - - tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' - danger = ColorScheme.hex_for_name('danger') || 'e45735' - - requests = filters.map do |filter| - color = report.rgba_color(tertiary) - - if filter == "page_view_anon" - color = report.rgba_color(tertiary, 0.5) - end - - if filter == "page_view_crawler" - color = report.rgba_color(danger, 0.75) - end - - { - req: filter, - label: I18n.t("reports.consolidated_page_views.xaxis.#{filter}"), - color: color, - data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) - } - end - - requests.each do |request| - request[:data] = request[:data].where('date >= ? AND date <= ?', report.start_date, report.end_date) - .order(date: :asc) - .group(:date) - .sum(:count) - .map { |date, count| { x: date, y: count } } - end - - report.data = requests - end - def self.req_report(report, filter = nil) data = if filter == :page_view_total @@ -287,198 +245,6 @@ class Report ).sum(:count) end - def self.report_visits(report) - report.group_filtering = true - report.icon = 'user' - - basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id - add_counts report, UserVisit, 'visited_at' - - report.prev30Days = UserVisit.where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count - end - - def self.report_mobile_visits(report) - basic_report_about report, UserVisit, :mobile_by_day, report.start_date, report.end_date - report.total = UserVisit.where(mobile: true).count - report.prev30Days = UserVisit.where(mobile: true).where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count - end - - def self.report_signups(report) - report.group_filtering = true - - report.icon = 'user-plus' - - if report.group_id - basic_report_about report, User.real, :count_by_signup_date, report.start_date, report.end_date, report.group_id - add_counts report, User.real, 'users.created_at' - else - report_about report, User.real, :count_by_signup_date - end - - # add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date - end - - def self.report_new_contributors(report) - report.data = [] - - data = User.real.count_by_first_post(report.start_date, report.end_date) - - if report.facets.include?(:prev30Days) - prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date) - report.prev30Days = prev30DaysData.sum { |k, v| v } - end - - if report.facets.include?(:total) - report.total = User.real.count_by_first_post - end - - if report.facets.include?(:prev_period) - prev_period_data = User.real.count_by_first_post(report.prev_start_date, report.prev_end_date) - report.prev_period = prev_period_data.sum { |k, v| v } - # report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } } - end - - data.each do |key, value| - report.data << { x: key, y: value } - end - end - - def self.report_daily_engaged_users(report) - report.average = true - - report.data = [] - - data = UserAction.count_daily_engaged_users(report.start_date, report.end_date) - - if report.facets.include?(:prev30Days) - prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date) - report.prev30Days = prev30DaysData.sum { |k, v| v } - end - - if report.facets.include?(:total) - report.total = UserAction.count_daily_engaged_users - end - - if report.facets.include?(:prev_period) - prev_data = UserAction.count_daily_engaged_users(report.prev_start_date, report.prev_end_date) - - prev = prev_data.sum { |k, v| v } - if prev > 0 - prev = prev / ((report.end_date - report.start_date) / 1.day) - end - report.prev_period = prev - end - - data.each do |key, value| - report.data << { x: key, y: value } - end - end - - def self.report_dau_by_mau(report) - report.labels = [ - { - type: :date, - property: :x, - title: I18n.t("reports.default.labels.day") - }, - { - type: :percent, - property: :y, - title: I18n.t("reports.default.labels.percent") - }, - ] - - report.average = true - report.percent = true - - data_points = UserVisit.count_by_active_users(report.start_date, report.end_date) - - report.data = [] - - compute_dau_by_mau = Proc.new { |data_point| - if data_point["mau"] == 0 - 0 - else - ((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil(2) - end - } - - dau_avg = Proc.new { |start_date, end_date| - data_points = UserVisit.count_by_active_users(start_date, end_date) - if !data_points.empty? - sum = data_points.sum { |data_point| compute_dau_by_mau.call(data_point) } - (sum.to_f / data_points.count.to_f).ceil(2) - end - } - - data_points.each do |data_point| - report.data << { x: data_point["date"], y: compute_dau_by_mau.call(data_point) } - end - - if report.facets.include?(:prev_period) - report.prev_period = dau_avg.call(report.prev_start_date, report.prev_end_date) - end - - if report.facets.include?(:prev30Days) - report.prev30Days = dau_avg.call(report.start_date - 30.days, report.start_date) - end - end - - def self.report_profile_views(report) - report.group_filtering = true - start_date = report.start_date - end_date = report.end_date - basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id - - report.total = UserProfile.sum(:views) - report.prev30Days = UserProfileView.where("viewed_at >= ? AND viewed_at < ?", start_date - 30.days, start_date + 1).count - end - - def self.report_topics(report) - report.category_filtering = true - basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id - countable = Topic.listable_topics - countable = countable.in_category_and_subcategories(report.category_id) if report.category_id - add_counts report, countable, 'topics.created_at' - end - - def self.report_posts(report) - report.modes = [:table, :chart] - report.category_filtering = true - basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, report.category_id - countable = Post.public_posts.where(post_type: Post.types[:regular]) - if report.category_id - countable = countable.joins(:topic).merge(Topic.in_category_and_subcategories(report.category_id)) - end - add_counts report, countable, 'posts.created_at' - end - - def self.report_time_to_first_response(report) - report.category_filtering = true - report.icon = 'reply' - report.higher_is_better = false - report.data = [] - Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r| - report.data << { x: r["date"], y: r["hours"].to_f.round(2) } - end - report.total = Topic.time_to_first_response_total(category_id: report.category_id) - report.prev30Days = Topic.time_to_first_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id) - end - - def self.report_topics_with_no_response(report) - report.category_filtering = true - report.data = [] - Topic.with_no_response_per_day(report.start_date, report.end_date, report.category_id).each do |r| - report.data << { x: r["date"], y: r["count"].to_i } - end - report.total = Topic.with_no_response_total(category_id: report.category_id) - report.prev30Days = Topic.with_no_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id) - end - - def self.report_emails(report) - report_about report, EmailLog - end - def self.report_about(report, subject_class, report_method = :count_per_day) basic_report_about report, subject_class, report_method, report.start_date, report.end_date add_counts report, subject_class @@ -521,65 +287,6 @@ class Report end end - def self.report_users_by_trust_level(report) - report.data = [] - - report.modes = [:table] - - report.dates_filtering = false - - report.labels = [ - { - property: :key, - title: I18n.t("reports.users_by_trust_level.labels.level") - }, - { - property: :y, - type: :number, - title: I18n.t("reports.default.labels.count") - } - ] - - User.real.group('trust_level').count.sort.each do |level, count| - key = TrustLevel.levels[level.to_i] - url = Proc.new { |k| "/admin/users/list/#{k}" } - report.data << { url: url.call(key), key: key, x: level.to_i, y: count } - end - end - - # Post action counts: - def self.report_flags(report) - report.category_filtering = true - report.icon = 'flag' - report.higher_is_better = false - - basic_report_about( - report, - ReviewableFlaggedPost, - :count_by_date, - report.start_date, - report.end_date, - report.category_id - ) - - countable = ReviewableFlaggedPost.scores_with_topics - countable.merge!(Topic.in_category_and_subcategories(report.category_id)) if report.category_id - - add_counts report, countable, 'reviewable_scores.created_at' - end - - def self.report_likes(report) - report.category_filtering = true - report.icon = 'heart' - post_action_report report, PostActionType.types[:like] - end - - def self.report_bookmarks(report) - report.category_filtering = true - report.icon = 'bookmark' - post_action_report report, PostActionType.types[:bookmark] - end - def self.post_action_report(report, post_action_type) report.data = [] PostAction.count_per_day_for_type(post_action_type, category_id: report.category_id, start_date: report.start_date, end_date: report.end_date).each do |date, count| @@ -598,1076 +305,6 @@ class Report add_counts report, subject, 'topics.created_at' end - def self.report_user_to_user_private_messages(report) - report.icon = 'envelope' - private_messages_report report, TopicSubtype.user_to_user - end - - def self.report_user_to_user_private_messages_with_replies(report) - report.icon = 'envelope' - topic_subtype = TopicSubtype.user_to_user - subject = Post.where('posts.user_id > 0') - basic_report_about report, subject, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype - subject = Post.private_posts.where('posts.user_id > 0').with_topic_subtype(topic_subtype) - add_counts report, subject, 'posts.created_at' - end - - def self.report_system_private_messages(report) - report.icon = 'envelope' - private_messages_report report, TopicSubtype.system_message - end - - def self.report_moderator_warning_private_messages(report) - report.icon = 'envelope' - private_messages_report report, TopicSubtype.moderator_warning - end - - def self.report_notify_moderators_private_messages(report) - report.icon = 'envelope' - private_messages_report report, TopicSubtype.notify_moderators - end - - def self.report_notify_user_private_messages(report) - report.icon = 'envelope' - private_messages_report report, TopicSubtype.notify_user - end - - def self.report_web_crawlers(report) - report.labels = [ - { - type: :string, - property: :user_agent, - title: I18n.t("reports.web_crawlers.labels.user_agent") - }, - { - property: :count, - type: :number, - title: I18n.t("reports.web_crawlers.labels.page_views") - } - ] - report.modes = [:table] - report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date) - .limit(200) - .order('sum_count DESC') - .group(:user_agent).sum(:count) - .map { |ua, count| { user_agent: ua, count: count } } - end - - def self.report_users_by_type(report) - report.data = [] - - report.modes = [:table] - - report.dates_filtering = false - - report.labels = [ - { - property: :x, - title: I18n.t("reports.users_by_type.labels.type") - }, - { - property: :y, - type: :number, - title: I18n.t("reports.default.labels.count") - } - ] - - label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") } - url = Proc.new { |key| "/admin/users/list/#{key}" } - - admins = User.real.admins.count - report.data << { url: url.call("admins"), icon: "shield-alt", key: "admins", x: label.call("admin"), y: admins } if admins > 0 - - moderators = User.real.moderators.count - report.data << { url: url.call("moderators"), icon: "shield-alt", key: "moderators", x: label.call("moderator"), y: moderators } if moderators > 0 - - suspended = User.real.suspended.count - report.data << { url: url.call("suspended"), icon: "ban", key: "suspended", x: label.call("suspended"), y: suspended } if suspended > 0 - - silenced = User.real.silenced.count - report.data << { url: url.call("silenced"), icon: "ban", key: "silenced", x: label.call("silenced"), y: silenced } if silenced > 0 - end - - def self.report_top_referred_topics(report) - report.category_filtering = true - report.modes = [:table] - - report.labels = [ - { - type: :topic, - properties: { - title: :topic_title, - id: :topic_id - }, - title: I18n.t("reports.top_referred_topics.labels.topic") - }, - { - property: :num_clicks, - type: :number, - title: I18n.t("reports.top_referred_topics.labels.num_clicks") - } - ] - - options = { - end_date: report.end_date, - start_date: report.start_date, - limit: report.limit || 8, - category_id: report.category_id - } - result = nil - result = IncomingLinksReport.find(:top_referred_topics, options) - report.data = result.data - end - - def self.report_top_traffic_sources(report) - report.category_filtering = true - report.modes = [:table] - - report.labels = [ - { - property: :domain, - title: I18n.t("reports.top_traffic_sources.labels.domain") - }, - { - property: :num_clicks, - type: :number, - title: I18n.t("reports.top_traffic_sources.labels.num_clicks") - }, - { - property: :num_topics, - type: :number, - title: I18n.t("reports.top_traffic_sources.labels.num_topics") - } - ] - - options = { - end_date: report.end_date, - start_date: report.start_date, - limit: report.limit || 8, - category_id: report.category_id - } - - result = IncomingLinksReport.find(:top_traffic_sources, options) - report.data = result.data - end - - def self.report_top_referrers(report) - report.modes = [:table] - - report.labels = [ - { - type: :user, - properties: { - username: :username, - id: :user_id, - avatar: :user_avatar_template, - }, - title: I18n.t("reports.top_referrers.labels.user") - }, - { - property: :num_clicks, - type: :number, - title: I18n.t("reports.top_referrers.labels.num_clicks") - }, - { - property: :num_topics, - type: :number, - title: I18n.t("reports.top_referrers.labels.num_topics") - } - ] - - options = { - end_date: report.end_date, - start_date: report.start_date, - limit: report.limit || 8 - } - - result = IncomingLinksReport.find(:top_referrers, options) - report.data = result.data - end - - def self.report_trending_search(report) - report.labels = [ - { - property: :term, - type: :text, - title: I18n.t("reports.trending_search.labels.term") - }, - { - property: :searches, - type: :number, - title: I18n.t("reports.trending_search.labels.searches") - }, - { - type: :percent, - property: :ctr, - title: I18n.t("reports.trending_search.labels.click_through") - } - ] - - report.data = [] - - report.modes = [:table] - - trends = SearchLog.trending_from(report.start_date, - end_date: report.end_date, - limit: report.limit - ) - - trends.each do |trend| - report.data << { - term: trend.term, - searches: trend.searches, - ctr: trend.ctr - } - end - end - - def self.report_moderators_activity(report) - report.labels = [ - { - type: :user, - properties: { - username: :username, - id: :user_id, - avatar: :user_avatar_template, - }, - title: I18n.t("reports.moderators_activity.labels.moderator"), - }, - { - property: :flag_count, - type: :number, - title: I18n.t("reports.moderators_activity.labels.flag_count") - }, - { - type: :seconds, - property: :time_read, - title: I18n.t("reports.moderators_activity.labels.time_read") - }, - { - property: :topic_count, - type: :number, - title: I18n.t("reports.moderators_activity.labels.topic_count") - }, - { - property: :pm_count, - type: :number, - title: I18n.t("reports.moderators_activity.labels.pm_count") - }, - { - property: :post_count, - type: :number, - title: I18n.t("reports.moderators_activity.labels.post_count") - }, - { - property: :revision_count, - type: :number, - title: I18n.t("reports.moderators_activity.labels.revision_count") - } - ] - - report.modes = [:table] - report.data = [] - - query = <<~SQL - WITH mods AS ( - SELECT - id AS user_id, - username_lower AS username, - uploaded_avatar_id - FROM users u - WHERE u.moderator = 'true' - AND u.id > 0 - ), - time_read AS ( - SELECT SUM(uv.time_read) AS time_read, - uv.user_id - FROM mods m - JOIN user_visits uv - ON m.user_id = uv.user_id - WHERE uv.visited_at >= '#{report.start_date}' - AND uv.visited_at <= '#{report.end_date}' - GROUP BY uv.user_id - ), - flag_count AS ( - WITH period_actions AS ( - SELECT agreed_by_id, - disagreed_by_id - FROM post_actions - WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(',')}) - AND created_at >= '#{report.start_date}' - AND created_at <= '#{report.end_date}' - ), - agreed_flags AS ( - SELECT pa.agreed_by_id AS user_id, - COUNT(*) AS flag_count - FROM mods m - JOIN period_actions pa - ON pa.agreed_by_id = m.user_id - GROUP BY agreed_by_id - ), - disagreed_flags AS ( - SELECT pa.disagreed_by_id AS user_id, - COUNT(*) AS flag_count - FROM mods m - JOIN period_actions pa - ON pa.disagreed_by_id = m.user_id - GROUP BY disagreed_by_id - ) - SELECT - COALESCE(af.user_id, df.user_id) AS user_id, - COALESCE(af.flag_count, 0) + COALESCE(df.flag_count, 0) AS flag_count - FROM agreed_flags af - FULL OUTER JOIN disagreed_flags df - ON df.user_id = af.user_id - ), - revision_count AS ( - SELECT pr.user_id, - COUNT(*) AS revision_count - FROM mods m - JOIN post_revisions pr - ON pr.user_id = m.user_id - JOIN posts p - ON p.id = pr.post_id - WHERE pr.created_at >= '#{report.start_date}' - AND pr.created_at <= '#{report.end_date}' - AND p.user_id <> pr.user_id - GROUP BY pr.user_id - ), - topic_count AS ( - SELECT t.user_id, - COUNT(*) AS topic_count - FROM mods m - JOIN topics t - ON t.user_id = m.user_id - WHERE t.archetype = 'regular' - AND t.created_at >= '#{report.start_date}' - AND t.created_at <= '#{report.end_date}' - GROUP BY t.user_id - ), - post_count AS ( - SELECT p.user_id, - COUNT(*) AS post_count - FROM mods m - JOIN posts p - ON p.user_id = m.user_id - JOIN topics t - ON t.id = p.topic_id - WHERE t.archetype = 'regular' - AND p.created_at >= '#{report.start_date}' - AND p.created_at <= '#{report.end_date}' - GROUP BY p.user_id - ), - pm_count AS ( - SELECT p.user_id, - COUNT(*) AS pm_count - FROM mods m - JOIN posts p - ON p.user_id = m.user_id - JOIN topics t - ON t.id = p.topic_id - WHERE t.archetype = 'private_message' - AND p.created_at >= '#{report.start_date}' - AND p.created_at <= '#{report.end_date}' - GROUP BY p.user_id - ) - - SELECT - m.user_id, - m.username, - m.uploaded_avatar_id, - tr.time_read, - fc.flag_count, - rc.revision_count, - tc.topic_count, - pc.post_count, - pmc.pm_count - FROM mods m - LEFT JOIN time_read tr ON tr.user_id = m.user_id - LEFT JOIN flag_count fc ON fc.user_id = m.user_id - LEFT JOIN revision_count rc ON rc.user_id = m.user_id - LEFT JOIN topic_count tc ON tc.user_id = m.user_id - LEFT JOIN post_count pc ON pc.user_id = m.user_id - LEFT JOIN pm_count pmc ON pmc.user_id = m.user_id - ORDER BY m.username - SQL - - DB.query(query).each do |row| - mod = {} - mod[:username] = row.username - mod[:user_id] = row.user_id - mod[:user_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - mod[:time_read] = row.time_read - mod[:flag_count] = row.flag_count - mod[:revision_count] = row.revision_count - mod[:topic_count] = row.topic_count - mod[:post_count] = row.post_count - mod[:pm_count] = row.pm_count - report.data << mod - end - end - - def self.report_flags_status(report) - report.modes = [:table] - - report.labels = [ - { - type: :post, - properties: { - topic_id: :topic_id, - number: :post_number, - truncated_raw: :post_type - }, - title: I18n.t("reports.flags_status.labels.flag") - }, - { - type: :user, - properties: { - username: :staff_username, - id: :staff_id, - avatar: :staff_avatar_template - }, - title: I18n.t("reports.flags_status.labels.assigned") - }, - { - type: :user, - properties: { - username: :poster_username, - id: :poster_id, - avatar: :poster_avatar_template - }, - title: I18n.t("reports.flags_status.labels.poster") - }, - { - type: :user, - properties: { - username: :flagger_username, - id: :flagger_id, - avatar: :flagger_avatar_template - }, - title: I18n.t("reports.flags_status.labels.flagger") - }, - { - type: :seconds, - property: :response_time, - title: I18n.t("reports.flags_status.labels.time_to_resolution") - } - ] - - report.data = [] - - flag_types = PostActionType.flag_types - - sql = <<~SQL - WITH period_actions AS ( - SELECT id, - post_action_type_id, - created_at, - agreed_at, - disagreed_at, - deferred_at, - agreed_by_id, - disagreed_by_id, - deferred_by_id, - post_id, - user_id, - COALESCE(disagreed_at, agreed_at, deferred_at) AS responded_at - FROM post_actions - WHERE post_action_type_id IN (#{flag_types.values.join(',')}) - AND created_at >= '#{report.start_date}' - AND created_at <= '#{report.end_date}' - ORDER BY created_at DESC - ), - poster_data AS ( - SELECT pa.id, - p.user_id AS poster_id, - p.topic_id as topic_id, - p.post_number as post_number, - u.username_lower AS poster_username, - u.uploaded_avatar_id AS poster_avatar_id - FROM period_actions pa - JOIN posts p - ON p.id = pa.post_id - JOIN users u - ON u.id = p.user_id - ), - flagger_data AS ( - SELECT pa.id, - u.id AS flagger_id, - u.username_lower AS flagger_username, - u.uploaded_avatar_id AS flagger_avatar_id - FROM period_actions pa - JOIN users u - ON u.id = pa.user_id - ), - staff_data AS ( - SELECT pa.id, - u.id AS staff_id, - u.username_lower AS staff_username, - u.uploaded_avatar_id AS staff_avatar_id - FROM period_actions pa - JOIN users u - ON u.id = COALESCE(pa.agreed_by_id, pa.disagreed_by_id, pa.deferred_by_id) - ) - SELECT - sd.staff_username, - sd.staff_id, - sd.staff_avatar_id, - pd.poster_username, - pd.poster_id, - pd.poster_avatar_id, - pd.post_number, - pd.topic_id, - fd.flagger_username, - fd.flagger_id, - fd.flagger_avatar_id, - pa.post_action_type_id, - pa.created_at, - pa.agreed_at, - pa.disagreed_at, - pa.deferred_at, - pa.agreed_by_id, - pa.disagreed_by_id, - pa.deferred_by_id, - COALESCE(pa.disagreed_at, pa.agreed_at, pa.deferred_at) AS responded_at - FROM period_actions pa - FULL OUTER JOIN staff_data sd - ON sd.id = pa.id - FULL OUTER JOIN flagger_data fd - ON fd.id = pa.id - FULL OUTER JOIN poster_data pd - ON pd.id = pa.id - SQL - - DB.query(sql).each do |row| - data = {} - - data[:post_type] = flag_types.key(row.post_action_type_id).to_s - data[:post_number] = row.post_number - data[:topic_id] = row.topic_id - - if row.staff_id - data[:staff_username] = row.staff_username - data[:staff_id] = row.staff_id - data[:staff_avatar_template] = User.avatar_template(row.staff_username, row.staff_avatar_id) - end - - if row.poster_id - data[:poster_username] = row.poster_username - data[:poster_id] = row.poster_id - data[:poster_avatar_template] = User.avatar_template(row.poster_username, row.poster_avatar_id) - end - - if row.flagger_id - data[:flagger_id] = row.flagger_id - data[:flagger_username] = row.flagger_username - data[:flagger_avatar_template] = User.avatar_template(row.flagger_username, row.flagger_avatar_id) - end - - if row.agreed_by_id - data[:resolution] = I18n.t("reports.flags_status.values.agreed") - elsif row.disagreed_by_id - data[:resolution] = I18n.t("reports.flags_status.values.disagreed") - elsif row.deferred_by_id - data[:resolution] = I18n.t("reports.flags_status.values.deferred") - else - data[:resolution] = I18n.t("reports.flags_status.values.no_action") - end - data[:response_time] = row.responded_at ? row.responded_at - row.created_at : nil - report.data << data - end - end - - def self.report_post_edits(report) - report.category_filtering = true - report.modes = [:table] - - report.labels = [ - { - type: :post, - properties: { - topic_id: :topic_id, - number: :post_number, - truncated_raw: :post_raw - }, - title: I18n.t("reports.post_edits.labels.post") - }, - { - type: :user, - properties: { - username: :editor_username, - id: :editor_id, - avatar: :editor_avatar_template, - }, - title: I18n.t("reports.post_edits.labels.editor") - }, - { - type: :user, - properties: { - username: :author_username, - id: :author_id, - avatar: :author_avatar_template, - }, - title: I18n.t("reports.post_edits.labels.author") - }, - { - type: :text, - property: :edit_reason, - title: I18n.t("reports.post_edits.labels.edit_reason") - }, - ] - - report.data = [] - - sql = <<~SQL - WITH period_revisions AS ( - SELECT pr.user_id AS editor_id, - pr.number AS revision_version, - pr.created_at, - pr.post_id, - u.username AS editor_username, - u.uploaded_avatar_id as editor_avatar_id - FROM post_revisions pr - JOIN users u - ON u.id = pr.user_id - WHERE u.id > 0 - AND pr.created_at >= '#{report.start_date}' - AND pr.created_at <= '#{report.end_date}' - ORDER BY pr.created_at DESC - LIMIT 20 - ) - SELECT pr.editor_id, - pr.editor_username, - pr.editor_avatar_id, - p.user_id AS author_id, - u.username AS author_username, - u.uploaded_avatar_id AS author_avatar_id, - pr.revision_version, - p.version AS post_version, - pr.post_id, - left(p.raw, 40) AS post_raw, - p.topic_id, - p.post_number, - p.edit_reason, - pr.created_at - FROM period_revisions pr - JOIN posts p - ON p.id = pr.post_id - JOIN users u - ON u.id = p.user_id - SQL - - if report.category_id - sql += <<~SQL - JOIN topics t - ON t.id = p.topic_id - WHERE t.category_id = ? OR t.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = ?) - SQL - end - result = report.category_id ? DB.query(sql, report.category_id, report.category_id) : DB.query(sql) - - result.each do |r| - revision = {} - revision[:editor_id] = r.editor_id - revision[:editor_username] = r.editor_username - revision[:editor_avatar_template] = User.avatar_template(r.editor_username, r.editor_avatar_id) - revision[:author_id] = r.author_id - revision[:author_username] = r.author_username - revision[:author_avatar_template] = User.avatar_template(r.author_username, r.author_avatar_id) - revision[:edit_reason] = r.revision_version == r.post_version ? r.edit_reason : nil - revision[:created_at] = r.created_at - revision[:post_raw] = r.post_raw - revision[:topic_id] = r.topic_id - revision[:post_number] = r.post_number - - report.data << revision - end - end - - def self.report_user_flagging_ratio(report) - report.data = [] - - report.modes = [:table] - - report.dates_filtering = true - - report.labels = [ - { - type: :user, - properties: { - username: :username, - id: :user_id, - avatar: :avatar_template, - }, - title: I18n.t("reports.user_flagging_ratio.labels.user") - }, - { - type: :number, - property: :disagreed_flags, - title: I18n.t("reports.user_flagging_ratio.labels.disagreed_flags") - }, - { - type: :number, - property: :agreed_flags, - title: I18n.t("reports.user_flagging_ratio.labels.agreed_flags") - }, - { - type: :number, - property: :ignored_flags, - title: I18n.t("reports.user_flagging_ratio.labels.ignored_flags") - }, - { - type: :number, - property: :score, - title: I18n.t("reports.user_flagging_ratio.labels.score") - }, - ] - - statuses = ReviewableScore.statuses - - agreed = "SUM(CASE WHEN rs.status = #{statuses[:agreed]} THEN 1 ELSE 0 END)::numeric" - disagreed = "SUM(CASE WHEN rs.status = #{statuses[:disagreed]} THEN 1 ELSE 0 END)::numeric" - ignored = "SUM(CASE WHEN rs.status = #{statuses[:ignored]} THEN 1 ELSE 0 END)::numeric" - - sql = <<~SQL - SELECT u.id, - u.username, - u.uploaded_avatar_id as avatar_id, - CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced, - #{disagreed} AS disagreed_flags, - #{agreed} AS agreed_flags, - #{ignored} AS ignored_flags, - ROUND((1-(#{agreed} / #{disagreed})) * (#{disagreed} - #{agreed})) AS score - FROM users AS u - INNER JOIN reviewable_scores AS rs ON rs.user_id = u.id - WHERE u.id > 0 - AND rs.created_at >= :start_date - AND rs.created_at <= :end_date - GROUP BY u.id, - u.username, - u.uploaded_avatar_id, - u.silenced_till - HAVING #{disagreed} > #{agreed} - ORDER BY score DESC - LIMIT 100 - SQL - - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - flagger = {} - flagger[:user_id] = row.id - flagger[:username] = row.username - flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) - flagger[:disagreed_flags] = row.disagreed_flags - flagger[:ignored_flags] = row.ignored_flags - flagger[:agreed_flags] = row.agreed_flags - flagger[:score] = row.score - - report.data << flagger - end - end - - def self.report_staff_logins(report) - report.modes = [:table] - - report.data = [] - - report.labels = [ - { - type: :user, - properties: { - username: :username, - id: :user_id, - avatar: :avatar_template, - }, - title: I18n.t("reports.staff_logins.labels.user") - }, - { - property: :location, - title: I18n.t("reports.staff_logins.labels.location") - }, - { - property: :created_at, - type: :precise_date, - title: I18n.t("reports.staff_logins.labels.login_at") - } - ] - - sql = <<~SQL - SELECT - t1.created_at created_at, - t1.client_ip client_ip, - u.username username, - u.uploaded_avatar_id uploaded_avatar_id, - u.id user_id - FROM ( - SELECT DISTINCT ON (t.client_ip, t.user_id) t.client_ip, t.user_id, t.created_at - FROM user_auth_token_logs t - WHERE t.user_id IN (#{User.admins.pluck(:id).join(',')}) - AND t.created_at >= :start_date - AND t.created_at <= :end_date - ORDER BY t.client_ip, t.user_id, t.created_at DESC - LIMIT #{report.limit || 20} - ) t1 - JOIN users u ON u.id = t1.user_id - ORDER BY created_at DESC - SQL - - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - data = {} - data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - data[:user_id] = row.user_id - data[:username] = row.username - data[:location] = DiscourseIpInfo.get(row.client_ip)[:location] - data[:created_at] = row.created_at - - report.data << data - end - end - - def self.report_suspicious_logins(report) - report.modes = [:table] - - report.labels = [ - { - type: :user, - properties: { - username: :username, - id: :user_id, - avatar: :avatar_template, - }, - title: I18n.t("reports.suspicious_logins.labels.user") - }, - { - property: :client_ip, - title: I18n.t("reports.suspicious_logins.labels.client_ip") - }, - { - property: :location, - title: I18n.t("reports.suspicious_logins.labels.location") - }, - { - property: :browser, - title: I18n.t("reports.suspicious_logins.labels.browser") - }, - { - property: :device, - title: I18n.t("reports.suspicious_logins.labels.device") - }, - { - property: :os, - title: I18n.t("reports.suspicious_logins.labels.os") - }, - { - type: :date, - property: :login_time, - title: I18n.t("reports.suspicious_logins.labels.login_time") - }, - ] - - report.data = [] - - sql = <<~SQL - SELECT u.id user_id, u.username, u.uploaded_avatar_id, t.client_ip, t.user_agent, t.created_at login_time - FROM user_auth_token_logs t - JOIN users u ON u.id = t.user_id - WHERE t.action = 'suspicious' - AND t.created_at >= :start_date - AND t.created_at <= :end_date - ORDER BY t.created_at DESC - SQL - - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - data = {} - - ipinfo = DiscourseIpInfo.get(row.client_ip) - browser = BrowserDetection.browser(row.user_agent) - device = BrowserDetection.device(row.user_agent) - os = BrowserDetection.os(row.user_agent) - - data[:username] = row.username - data[:user_id] = row.user_id - data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - data[:client_ip] = row.client_ip.to_s - data[:location] = ipinfo[:location] - data[:browser] = I18n.t("user_auth_tokens.browser.#{browser}") - data[:device] = I18n.t("user_auth_tokens.device.#{device}") - data[:os] = I18n.t("user_auth_tokens.os.#{os}") - data[:login_time] = row.login_time - - report.data << data - end - end - - def self.report_storage_stats(report) - backup_stats = begin - BackupRestore::BackupStore.create.stats - rescue BackupRestore::BackupStore::StorageError - nil - end - - report.data = { - backups: backup_stats, - uploads: { - used_bytes: DiskSpace.uploads_used_bytes, - free_bytes: DiskSpace.uploads_free_bytes - } - } - end - - def self.report_top_uploads(report) - report.modes = [:table] - report.filter_options = [ - { - id: "file-extension", - selected: report.filter_values.fetch("file-extension", "any"), - choices: (SiteSetting.authorized_extensions.split("|") + report.filter_values.values).uniq, - allowAny: true - } - ] - report.labels = [ - { - type: :link, - properties: [ - :file_url, - :file_name, - ], - title: I18n.t("reports.top_uploads.labels.filename") - }, - { - type: :user, - properties: { - username: :author_username, - id: :author_id, - avatar: :author_avatar_template, - }, - title: I18n.t("reports.top_uploads.labels.author") - }, - { - type: :text, - property: :extension, - title: I18n.t("reports.top_uploads.labels.extension") - }, - { - type: :bytes, - property: :filesize, - title: I18n.t("reports.top_uploads.labels.filesize") - }, - ] - - report.data = [] - - sql = <<~SQL - SELECT - u.id as user_id, - u.username, - u.uploaded_avatar_id, - up.filesize, - up.original_filename, - up.extension, - up.url - FROM uploads up - JOIN users u - ON u.id = up.user_id - /*where*/ - ORDER BY up.filesize DESC - LIMIT #{report.limit || 250} - SQL - - extension_filter = report.filter_values["file-extension"] - builder = DB.build(sql) - builder.where("up.id > :seeded_id_threshold", seeded_id_threshold: Upload::SEEDED_ID_THRESHOLD) - builder.where("up.created_at >= :start_date", start_date: report.start_date) - builder.where("up.created_at < :end_date", end_date: report.end_date) - builder.where("up.extension = :extension", extension: extension_filter) if extension_filter.present? - builder.query.each do |row| - data = {} - data[:author_id] = row.user_id - data[:author_username] = row.username - data[:author_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - data[:filesize] = row.filesize - data[:extension] = row.extension - data[:file_url] = Discourse.store.cdn_url(row.url) - data[:file_name] = row.original_filename.truncate(25) - report.data << data - end - end - - def self.report_top_ignored_users(report) - report.modes = [:table] - - report.labels = [ - { - type: :user, - properties: { - id: :ignored_user_id, - username: :ignored_username, - avatar: :ignored_user_avatar_template, - }, - title: I18n.t("reports.top_ignored_users.labels.ignored_user") - }, - { - type: :number, - properties: [ - :ignores_count, - ], - title: I18n.t("reports.top_ignored_users.labels.ignores_count") - }, - { - type: :number, - properties: [ - :mutes_count, - ], - title: I18n.t("reports.top_ignored_users.labels.mutes_count") - } - ] - - report.data = [] - - sql = <<~SQL - WITH ignored_users AS ( - SELECT - ignored_user_id as user_id, - COUNT(*) AS ignores_count - FROM ignored_users - WHERE created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' - GROUP BY ignored_user_id - ORDER BY COUNT(*) DESC - LIMIT :limit - ), - muted_users AS ( - SELECT - muted_user_id as user_id, - COUNT(*) AS mutes_count - FROM muted_users - WHERE created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' - GROUP BY muted_user_id - ORDER BY COUNT(*) DESC - LIMIT :limit - ) - - SELECT u.id as user_id, - u.username as username, - u.uploaded_avatar_id as uploaded_avatar_id, - ig.ignores_count as ignores_count, - COALESCE(mu.mutes_count, 0) as mutes_count, - ig.ignores_count + COALESCE(mu.mutes_count, 0) as total - FROM users as u - JOIN ignored_users as ig ON ig.user_id = u.id - LEFT OUTER JOIN muted_users as mu ON mu.user_id = u.id - ORDER BY total DESC - SQL - - DB.query(sql, limit: report.limit || 250).each do |row| - report.data << { - ignored_user_id: row.user_id, - ignored_username: row.username, - ignored_user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), - ignores_count: row.ignores_count, - mutes_count: row.mutes_count, - } - end - end - DiscourseEvent.on(:site_setting_saved) do |site_setting| if ["backup_location", "s3_backup_bucket"].include?(site_setting.name.to_s) clear_cache(:storage_stats) @@ -1699,3 +336,41 @@ class Report .map! { |rgb| rgb.to_i } end end + +require_relative "reports/visits" +require_relative "reports/visits_mobile" +require_relative "reports/consolidated_page_views" +require_relative "reports/top_ignored_users" +require_relative "reports/top_uploads" +require_relative "reports/moderators_activity" +require_relative "reports/signups" +require_relative "reports/storage_stats" +require_relative "reports/suspicious_logins" +require_relative "reports/new_contributors" +require_relative "reports/users_by_trust_level" +require_relative "reports/staff_logins" +require_relative "reports/users_by_type" +require_relative "reports/user_flagging_ratio" +require_relative "reports/post_edits" +require_relative "reports/daily_engaged_users" +require_relative "reports/flags_status" +require_relative "reports/trending_search" +require_relative "reports/top_referrers" +require_relative "reports/top_traffic_sources" +require_relative "reports/top_referred_topics" +require_relative "reports/notify_user_private_messages" +require_relative "reports/user_to_user_private_messages" +require_relative "reports/user_to_user_private_messages_with_replies" +require_relative "reports/system_private_messages" +require_relative "reports/moderator_warning_private_messages" +require_relative "reports/notify_moderators_private_messages" +require_relative "reports/flags" +require_relative "reports/likes" +require_relative "reports/bookmarks" +require_relative "reports/dau_by_mau" +require_relative "reports/profile_views" +require_relative "reports/topics" +require_relative "reports/posts" +require_relative "reports/time_to_first_response" +require_relative "reports/topics_with_no_response" +require_relative "reports/emails" diff --git a/app/models/reports/bookmarks.rb b/app/models/reports/bookmarks.rb new file mode 100644 index 00000000000..1752b560ccb --- /dev/null +++ b/app/models/reports/bookmarks.rb @@ -0,0 +1,5 @@ +Report.add_report("bookmarks") do |report| + report.category_filtering = true + report.icon = 'bookmark' + post_action_report report, PostActionType.types[:bookmark] +end diff --git a/app/models/reports/consolidated_page_views.rb b/app/models/reports/consolidated_page_views.rb new file mode 100644 index 00000000000..adb8f68b673 --- /dev/null +++ b/app/models/reports/consolidated_page_views.rb @@ -0,0 +1,41 @@ +Report.add_report("consolidated_page_views") do |report| + filters = %w[ + page_view_logged_in + page_view_anon + page_view_crawler + ] + + report.modes = [:stacked_chart] + + tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' + danger = ColorScheme.hex_for_name('danger') || 'e45735' + + requests = filters.map do |filter| + color = report.rgba_color(tertiary) + + if filter == "page_view_anon" + color = report.rgba_color(tertiary, 0.5) + end + + if filter == "page_view_crawler" + color = report.rgba_color(danger, 0.75) + end + + { + req: filter, + label: I18n.t("reports.consolidated_page_views.xaxis.#{filter}"), + color: color, + data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) + } + end + + requests.each do |request| + request[:data] = request[:data].where('date >= ? AND date <= ?', report.start_date, report.end_date) + .order(date: :asc) + .group(:date) + .sum(:count) + .map { |date, count| { x: date, y: count } } + end + + report.data = requests +end diff --git a/app/models/reports/daily_engaged_users.rb b/app/models/reports/daily_engaged_users.rb new file mode 100644 index 00000000000..0b26e91cebc --- /dev/null +++ b/app/models/reports/daily_engaged_users.rb @@ -0,0 +1,30 @@ +Report.add_report("daily_engaged_users") do |report| + report.average = true + + report.data = [] + + data = UserAction.count_daily_engaged_users(report.start_date, report.end_date) + + if report.facets.include?(:prev30Days) + prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date) + report.prev30Days = prev30DaysData.sum { |k, v| v } + end + + if report.facets.include?(:total) + report.total = UserAction.count_daily_engaged_users + end + + if report.facets.include?(:prev_period) + prev_data = UserAction.count_daily_engaged_users(report.prev_start_date, report.prev_end_date) + + prev = prev_data.sum { |k, v| v } + if prev > 0 + prev = prev / ((report.end_date - report.start_date) / 1.day) + end + report.prev_period = prev + end + + data.each do |key, value| + report.data << { x: key, y: value } + end +end diff --git a/app/models/reports/dau_by_mau.rb b/app/models/reports/dau_by_mau.rb new file mode 100644 index 00000000000..6fc75124241 --- /dev/null +++ b/app/models/reports/dau_by_mau.rb @@ -0,0 +1,49 @@ +Report.add_report("dau_by_mau") do |report| + report.labels = [ + { + type: :date, + property: :x, + title: I18n.t("reports.default.labels.day") + }, + { + type: :percent, + property: :y, + title: I18n.t("reports.default.labels.percent") + }, + ] + + report.average = true + report.percent = true + + data_points = UserVisit.count_by_active_users(report.start_date, report.end_date) + + report.data = [] + + compute_dau_by_mau = Proc.new { |data_point| + if data_point["mau"] == 0 + 0 + else + ((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil(2) + end + } + + dau_avg = Proc.new { |start_date, end_date| + data_points = UserVisit.count_by_active_users(start_date, end_date) + if !data_points.empty? + sum = data_points.sum { |data_point| compute_dau_by_mau.call(data_point) } + (sum.to_f / data_points.count.to_f).ceil(2) + end + } + + data_points.each do |data_point| + report.data << { x: data_point["date"], y: compute_dau_by_mau.call(data_point) } + end + + if report.facets.include?(:prev_period) + report.prev_period = dau_avg.call(report.prev_start_date, report.prev_end_date) + end + + if report.facets.include?(:prev30Days) + report.prev30Days = dau_avg.call(report.start_date - 30.days, report.start_date) + end +end diff --git a/app/models/reports/emails.rb b/app/models/reports/emails.rb new file mode 100644 index 00000000000..6ccb593cdeb --- /dev/null +++ b/app/models/reports/emails.rb @@ -0,0 +1,3 @@ +Report.add_report("emails") do |report| + report_about report, EmailLog +end diff --git a/app/models/reports/flags.rb b/app/models/reports/flags.rb new file mode 100644 index 00000000000..8d66da9fd0f --- /dev/null +++ b/app/models/reports/flags.rb @@ -0,0 +1,19 @@ +Report.add_report("flags") do |report| + report.category_filtering = true + report.icon = 'flag' + report.higher_is_better = false + + basic_report_about( + report, + ReviewableFlaggedPost, + :count_by_date, + report.start_date, + report.end_date, + report.category_id + ) + + countable = ReviewableFlaggedPost.scores_with_topics + countable.merge!(Topic.in_category_and_subcategories(report.category_id)) if report.category_id + + add_counts report, countable, 'reviewable_scores.created_at' +end diff --git a/app/models/reports/flags_status.rb b/app/models/reports/flags_status.rb new file mode 100644 index 00000000000..885b37b7f14 --- /dev/null +++ b/app/models/reports/flags_status.rb @@ -0,0 +1,170 @@ +Report.add_report("flags_status") do |report| + report.modes = [:table] + + report.labels = [ + { + type: :post, + properties: { + topic_id: :topic_id, + number: :post_number, + truncated_raw: :post_type + }, + title: I18n.t("reports.flags_status.labels.flag") + }, + { + type: :user, + properties: { + username: :staff_username, + id: :staff_id, + avatar: :staff_avatar_template + }, + title: I18n.t("reports.flags_status.labels.assigned") + }, + { + type: :user, + properties: { + username: :poster_username, + id: :poster_id, + avatar: :poster_avatar_template + }, + title: I18n.t("reports.flags_status.labels.poster") + }, + { + type: :user, + properties: { + username: :flagger_username, + id: :flagger_id, + avatar: :flagger_avatar_template + }, + title: I18n.t("reports.flags_status.labels.flagger") + }, + { + type: :seconds, + property: :response_time, + title: I18n.t("reports.flags_status.labels.time_to_resolution") + } + ] + + report.data = [] + + flag_types = PostActionType.flag_types + + sql = <<~SQL + WITH period_actions AS ( + SELECT id, + post_action_type_id, + created_at, + agreed_at, + disagreed_at, + deferred_at, + agreed_by_id, + disagreed_by_id, + deferred_by_id, + post_id, + user_id, + COALESCE(disagreed_at, agreed_at, deferred_at) AS responded_at + FROM post_actions + WHERE post_action_type_id IN (#{flag_types.values.join(',')}) + AND created_at >= '#{report.start_date}' + AND created_at <= '#{report.end_date}' + ORDER BY created_at DESC + ), + poster_data AS ( + SELECT pa.id, + p.user_id AS poster_id, + p.topic_id as topic_id, + p.post_number as post_number, + u.username_lower AS poster_username, + u.uploaded_avatar_id AS poster_avatar_id + FROM period_actions pa + JOIN posts p + ON p.id = pa.post_id + JOIN users u + ON u.id = p.user_id + ), + flagger_data AS ( + SELECT pa.id, + u.id AS flagger_id, + u.username_lower AS flagger_username, + u.uploaded_avatar_id AS flagger_avatar_id + FROM period_actions pa + JOIN users u + ON u.id = pa.user_id + ), + staff_data AS ( + SELECT pa.id, + u.id AS staff_id, + u.username_lower AS staff_username, + u.uploaded_avatar_id AS staff_avatar_id + FROM period_actions pa + JOIN users u + ON u.id = COALESCE(pa.agreed_by_id, pa.disagreed_by_id, pa.deferred_by_id) + ) + SELECT + sd.staff_username, + sd.staff_id, + sd.staff_avatar_id, + pd.poster_username, + pd.poster_id, + pd.poster_avatar_id, + pd.post_number, + pd.topic_id, + fd.flagger_username, + fd.flagger_id, + fd.flagger_avatar_id, + pa.post_action_type_id, + pa.created_at, + pa.agreed_at, + pa.disagreed_at, + pa.deferred_at, + pa.agreed_by_id, + pa.disagreed_by_id, + pa.deferred_by_id, + COALESCE(pa.disagreed_at, pa.agreed_at, pa.deferred_at) AS responded_at + FROM period_actions pa + FULL OUTER JOIN staff_data sd + ON sd.id = pa.id + FULL OUTER JOIN flagger_data fd + ON fd.id = pa.id + FULL OUTER JOIN poster_data pd + ON pd.id = pa.id + SQL + + DB.query(sql).each do |row| + data = {} + + data[:post_type] = flag_types.key(row.post_action_type_id).to_s + data[:post_number] = row.post_number + data[:topic_id] = row.topic_id + + if row.staff_id + data[:staff_username] = row.staff_username + data[:staff_id] = row.staff_id + data[:staff_avatar_template] = User.avatar_template(row.staff_username, row.staff_avatar_id) + end + + if row.poster_id + data[:poster_username] = row.poster_username + data[:poster_id] = row.poster_id + data[:poster_avatar_template] = User.avatar_template(row.poster_username, row.poster_avatar_id) + end + + if row.flagger_id + data[:flagger_id] = row.flagger_id + data[:flagger_username] = row.flagger_username + data[:flagger_avatar_template] = User.avatar_template(row.flagger_username, row.flagger_avatar_id) + end + + if row.agreed_by_id + data[:resolution] = I18n.t("reports.flags_status.values.agreed") + elsif row.disagreed_by_id + data[:resolution] = I18n.t("reports.flags_status.values.disagreed") + elsif row.deferred_by_id + data[:resolution] = I18n.t("reports.flags_status.values.deferred") + else + data[:resolution] = I18n.t("reports.flags_status.values.no_action") + end + data[:response_time] = row.responded_at ? row.responded_at - row.created_at : nil + report.data << data + end +end diff --git a/app/models/reports/likes.rb b/app/models/reports/likes.rb new file mode 100644 index 00000000000..0212da61e0d --- /dev/null +++ b/app/models/reports/likes.rb @@ -0,0 +1,5 @@ +Report.add_report("likes") do |report| + report.category_filtering = true + report.icon = 'heart' + post_action_report report, PostActionType.types[:like] +end diff --git a/app/models/reports/moderator_warning_private_messages.rb b/app/models/reports/moderator_warning_private_messages.rb new file mode 100644 index 00000000000..cfb5dcec778 --- /dev/null +++ b/app/models/reports/moderator_warning_private_messages.rb @@ -0,0 +1,4 @@ +Report.add_report("moderator_warning_private_messages") do |report| + report.icon = 'envelope' + private_messages_report report, TopicSubtype.moderator_warning +end diff --git a/app/models/reports/moderators_activity.rb b/app/models/reports/moderators_activity.rb new file mode 100644 index 00000000000..08b50337aed --- /dev/null +++ b/app/models/reports/moderators_activity.rb @@ -0,0 +1,183 @@ +Report.add_report("moderators_activity") do |report| + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :user_avatar_template, + }, + title: I18n.t("reports.moderators_activity.labels.moderator"), + }, + { + property: :flag_count, + type: :number, + title: I18n.t("reports.moderators_activity.labels.flag_count") + }, + { + type: :seconds, + property: :time_read, + title: I18n.t("reports.moderators_activity.labels.time_read") + }, + { + property: :topic_count, + type: :number, + title: I18n.t("reports.moderators_activity.labels.topic_count") + }, + { + property: :pm_count, + type: :number, + title: I18n.t("reports.moderators_activity.labels.pm_count") + }, + { + property: :post_count, + type: :number, + title: I18n.t("reports.moderators_activity.labels.post_count") + }, + { + property: :revision_count, + type: :number, + title: I18n.t("reports.moderators_activity.labels.revision_count") + } + ] + + report.modes = [:table] + report.data = [] + + query = <<~SQL + WITH mods AS ( + SELECT + id AS user_id, + username_lower AS username, + uploaded_avatar_id + FROM users u + WHERE u.moderator = 'true' + AND u.id > 0 + ), + time_read AS ( + SELECT SUM(uv.time_read) AS time_read, + uv.user_id + FROM mods m + JOIN user_visits uv + ON m.user_id = uv.user_id + WHERE uv.visited_at >= '#{report.start_date}' + AND uv.visited_at <= '#{report.end_date}' + GROUP BY uv.user_id + ), + flag_count AS ( + WITH period_actions AS ( + SELECT agreed_by_id, + disagreed_by_id + FROM post_actions + WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(',')}) + AND created_at >= '#{report.start_date}' + AND created_at <= '#{report.end_date}' + ), + agreed_flags AS ( + SELECT pa.agreed_by_id AS user_id, + COUNT(*) AS flag_count + FROM mods m + JOIN period_actions pa + ON pa.agreed_by_id = m.user_id + GROUP BY agreed_by_id + ), + disagreed_flags AS ( + SELECT pa.disagreed_by_id AS user_id, + COUNT(*) AS flag_count + FROM mods m + JOIN period_actions pa + ON pa.disagreed_by_id = m.user_id + GROUP BY disagreed_by_id + ) + SELECT + COALESCE(af.user_id, df.user_id) AS user_id, + COALESCE(af.flag_count, 0) + COALESCE(df.flag_count, 0) AS flag_count + FROM agreed_flags af + FULL OUTER JOIN disagreed_flags df + ON df.user_id = af.user_id + ), + revision_count AS ( + SELECT pr.user_id, + COUNT(*) AS revision_count + FROM mods m + JOIN post_revisions pr + ON pr.user_id = m.user_id + JOIN posts p + ON p.id = pr.post_id + WHERE pr.created_at >= '#{report.start_date}' + AND pr.created_at <= '#{report.end_date}' + AND p.user_id <> pr.user_id + GROUP BY pr.user_id + ), + topic_count AS ( + SELECT t.user_id, + COUNT(*) AS topic_count + FROM mods m + JOIN topics t + ON t.user_id = m.user_id + WHERE t.archetype = 'regular' + AND t.created_at >= '#{report.start_date}' + AND t.created_at <= '#{report.end_date}' + GROUP BY t.user_id + ), + post_count AS ( + SELECT p.user_id, + COUNT(*) AS post_count + FROM mods m + JOIN posts p + ON p.user_id = m.user_id + JOIN topics t + ON t.id = p.topic_id + WHERE t.archetype = 'regular' + AND p.created_at >= '#{report.start_date}' + AND p.created_at <= '#{report.end_date}' + GROUP BY p.user_id + ), + pm_count AS ( + SELECT p.user_id, + COUNT(*) AS pm_count + FROM mods m + JOIN posts p + ON p.user_id = m.user_id + JOIN topics t + ON t.id = p.topic_id + WHERE t.archetype = 'private_message' + AND p.created_at >= '#{report.start_date}' + AND p.created_at <= '#{report.end_date}' + GROUP BY p.user_id + ) + + SELECT + m.user_id, + m.username, + m.uploaded_avatar_id, + tr.time_read, + fc.flag_count, + rc.revision_count, + tc.topic_count, + pc.post_count, + pmc.pm_count + FROM mods m + LEFT JOIN time_read tr ON tr.user_id = m.user_id + LEFT JOIN flag_count fc ON fc.user_id = m.user_id + LEFT JOIN revision_count rc ON rc.user_id = m.user_id + LEFT JOIN topic_count tc ON tc.user_id = m.user_id + LEFT JOIN post_count pc ON pc.user_id = m.user_id + LEFT JOIN pm_count pmc ON pmc.user_id = m.user_id + ORDER BY m.username + SQL + + DB.query(query).each do |row| + mod = {} + mod[:username] = row.username + mod[:user_id] = row.user_id + mod[:user_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + mod[:time_read] = row.time_read + mod[:flag_count] = row.flag_count + mod[:revision_count] = row.revision_count + mod[:topic_count] = row.topic_count + mod[:post_count] = row.post_count + mod[:pm_count] = row.pm_count + report.data << mod + end +end diff --git a/app/models/reports/new_contributors.rb b/app/models/reports/new_contributors.rb new file mode 100644 index 00000000000..4619e33dced --- /dev/null +++ b/app/models/reports/new_contributors.rb @@ -0,0 +1,24 @@ +Report.add_report("new_contributors") do |report| + report.data = [] + + data = User.real.count_by_first_post(report.start_date, report.end_date) + + if report.facets.include?(:prev30Days) + prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date) + report.prev30Days = prev30DaysData.sum { |k, v| v } + end + + if report.facets.include?(:total) + report.total = User.real.count_by_first_post + end + + if report.facets.include?(:prev_period) + prev_period_data = User.real.count_by_first_post(report.prev_start_date, report.prev_end_date) + report.prev_period = prev_period_data.sum { |k, v| v } + # report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } } + end + + data.each do |key, value| + report.data << { x: key, y: value } + end +end diff --git a/app/models/reports/notify_moderators_private_messages.rb b/app/models/reports/notify_moderators_private_messages.rb new file mode 100644 index 00000000000..55e1cf9ae99 --- /dev/null +++ b/app/models/reports/notify_moderators_private_messages.rb @@ -0,0 +1,4 @@ +Report.add_report("notify_moderators_private_messages") do |report| + report.icon = 'envelope' + private_messages_report report, TopicSubtype.notify_moderators +end diff --git a/app/models/reports/notify_user_private_messages.rb b/app/models/reports/notify_user_private_messages.rb new file mode 100644 index 00000000000..900fbd3e1a6 --- /dev/null +++ b/app/models/reports/notify_user_private_messages.rb @@ -0,0 +1,4 @@ +Report.add_report("notify_user_private_messages") do |report| + report.icon = 'envelope' + private_messages_report report, TopicSubtype.notify_user +end diff --git a/app/models/reports/post_edits.rb b/app/models/reports/post_edits.rb new file mode 100644 index 00000000000..a708f4345af --- /dev/null +++ b/app/models/reports/post_edits.rb @@ -0,0 +1,105 @@ +Report.add_report("post_edits") do |report| + report.category_filtering = true + report.modes = [:table] + + report.labels = [ + { + type: :post, + properties: { + topic_id: :topic_id, + number: :post_number, + truncated_raw: :post_raw + }, + title: I18n.t("reports.post_edits.labels.post") + }, + { + type: :user, + properties: { + username: :editor_username, + id: :editor_id, + avatar: :editor_avatar_template, + }, + title: I18n.t("reports.post_edits.labels.editor") + }, + { + type: :user, + properties: { + username: :author_username, + id: :author_id, + avatar: :author_avatar_template, + }, + title: I18n.t("reports.post_edits.labels.author") + }, + { + type: :text, + property: :edit_reason, + title: I18n.t("reports.post_edits.labels.edit_reason") + }, + ] + + report.data = [] + + sql = <<~SQL + WITH period_revisions AS ( + SELECT pr.user_id AS editor_id, + pr.number AS revision_version, + pr.created_at, + pr.post_id, + u.username AS editor_username, + u.uploaded_avatar_id as editor_avatar_id + FROM post_revisions pr + JOIN users u + ON u.id = pr.user_id + WHERE u.id > 0 + AND pr.created_at >= '#{report.start_date}' + AND pr.created_at <= '#{report.end_date}' + ORDER BY pr.created_at DESC + LIMIT 20 + ) + SELECT pr.editor_id, + pr.editor_username, + pr.editor_avatar_id, + p.user_id AS author_id, + u.username AS author_username, + u.uploaded_avatar_id AS author_avatar_id, + pr.revision_version, + p.version AS post_version, + pr.post_id, + left(p.raw, 40) AS post_raw, + p.topic_id, + p.post_number, + p.edit_reason, + pr.created_at + FROM period_revisions pr + JOIN posts p + ON p.id = pr.post_id + JOIN users u + ON u.id = p.user_id + SQL + + if report.category_id + sql += <<~SQL + JOIN topics t + ON t.id = p.topic_id + WHERE t.category_id = ? OR t.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = ?) + SQL + end + result = report.category_id ? DB.query(sql, report.category_id, report.category_id) : DB.query(sql) + + result.each do |r| + revision = {} + revision[:editor_id] = r.editor_id + revision[:editor_username] = r.editor_username + revision[:editor_avatar_template] = User.avatar_template(r.editor_username, r.editor_avatar_id) + revision[:author_id] = r.author_id + revision[:author_username] = r.author_username + revision[:author_avatar_template] = User.avatar_template(r.author_username, r.author_avatar_id) + revision[:edit_reason] = r.revision_version == r.post_version ? r.edit_reason : nil + revision[:created_at] = r.created_at + revision[:post_raw] = r.post_raw + revision[:topic_id] = r.topic_id + revision[:post_number] = r.post_number + + report.data << revision + end +end diff --git a/app/models/reports/posts.rb b/app/models/reports/posts.rb new file mode 100644 index 00000000000..7e7e1f59c0c --- /dev/null +++ b/app/models/reports/posts.rb @@ -0,0 +1,10 @@ +Report.add_report("posts") do |report| + report.modes = [:table, :chart] + report.category_filtering = true + basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, report.category_id + countable = Post.public_posts.where(post_type: Post.types[:regular]) + if report.category_id + countable = countable.joins(:topic).merge(Topic.in_category_and_subcategories(report.category_id)) + end + add_counts report, countable, 'posts.created_at' +end diff --git a/app/models/reports/profile_views.rb b/app/models/reports/profile_views.rb new file mode 100644 index 00000000000..a9123da0091 --- /dev/null +++ b/app/models/reports/profile_views.rb @@ -0,0 +1,9 @@ +Report.add_report("profile_views") do |report| + report.group_filtering = true + start_date = report.start_date + end_date = report.end_date + basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, report.group_id + + report.total = UserProfile.sum(:views) + report.prev30Days = UserProfileView.where("viewed_at >= ? AND viewed_at < ?", start_date - 30.days, start_date + 1).count +end diff --git a/app/models/reports/signups.rb b/app/models/reports/signups.rb new file mode 100644 index 00000000000..e9646cd8c63 --- /dev/null +++ b/app/models/reports/signups.rb @@ -0,0 +1,14 @@ +Report.add_report("signups") do |report| + report.group_filtering = true + + report.icon = 'user-plus' + + if report.group_id + basic_report_about report, User.real, :count_by_signup_date, report.start_date, report.end_date, report.group_id + add_counts report, User.real, 'users.created_at' + else + report_about report, User.real, :count_by_signup_date + end + + # add_prev_data report, User.real, :count_by_signup_date, report.prev_start_date, report.prev_end_date +end diff --git a/app/models/reports/staff_logins.rb b/app/models/reports/staff_logins.rb new file mode 100644 index 00000000000..f41b3aa98dd --- /dev/null +++ b/app/models/reports/staff_logins.rb @@ -0,0 +1,57 @@ +Report.add_report("staff_logins") do |report| + report.modes = [:table] + + report.data = [] + + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :avatar_template, + }, + title: I18n.t("reports.staff_logins.labels.user") + }, + { + property: :location, + title: I18n.t("reports.staff_logins.labels.location") + }, + { + property: :created_at, + type: :precise_date, + title: I18n.t("reports.staff_logins.labels.login_at") + } + ] + + sql = <<~SQL + SELECT + t1.created_at created_at, + t1.client_ip client_ip, + u.username username, + u.uploaded_avatar_id uploaded_avatar_id, + u.id user_id + FROM ( + SELECT DISTINCT ON (t.client_ip, t.user_id) t.client_ip, t.user_id, t.created_at + FROM user_auth_token_logs t + WHERE t.user_id IN (#{User.admins.pluck(:id).join(',')}) + AND t.created_at >= :start_date + AND t.created_at <= :end_date + ORDER BY t.client_ip, t.user_id, t.created_at DESC + LIMIT #{report.limit || 20} + ) t1 + JOIN users u ON u.id = t1.user_id + ORDER BY created_at DESC + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + data = {} + data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + data[:user_id] = row.user_id + data[:username] = row.username + data[:location] = DiscourseIpInfo.get(row.client_ip)[:location] + data[:created_at] = row.created_at + + report.data << data + end +end diff --git a/app/models/reports/storage_stats.rb b/app/models/reports/storage_stats.rb new file mode 100644 index 00000000000..d178d84e328 --- /dev/null +++ b/app/models/reports/storage_stats.rb @@ -0,0 +1,15 @@ +Report.add_report("storage_stats") do |report| + backup_stats = begin + BackupRestore::BackupStore.create.stats + rescue BackupRestore::BackupStore::StorageError + nil + end + + report.data = { + backups: backup_stats, + uploads: { + used_bytes: DiskSpace.uploads_used_bytes, + free_bytes: DiskSpace.uploads_free_bytes + } + } +end diff --git a/app/models/reports/suspicious_logins.rb b/app/models/reports/suspicious_logins.rb new file mode 100644 index 00000000000..d805ba138f4 --- /dev/null +++ b/app/models/reports/suspicious_logins.rb @@ -0,0 +1,73 @@ +Report.add_report("suspicious_logins") do |report| + report.modes = [:table] + + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :avatar_template, + }, + title: I18n.t("reports.suspicious_logins.labels.user") + }, + { + property: :client_ip, + title: I18n.t("reports.suspicious_logins.labels.client_ip") + }, + { + property: :location, + title: I18n.t("reports.suspicious_logins.labels.location") + }, + { + property: :browser, + title: I18n.t("reports.suspicious_logins.labels.browser") + }, + { + property: :device, + title: I18n.t("reports.suspicious_logins.labels.device") + }, + { + property: :os, + title: I18n.t("reports.suspicious_logins.labels.os") + }, + { + type: :date, + property: :login_time, + title: I18n.t("reports.suspicious_logins.labels.login_time") + }, + ] + + report.data = [] + + sql = <<~SQL + SELECT u.id user_id, u.username, u.uploaded_avatar_id, t.client_ip, t.user_agent, t.created_at login_time + FROM user_auth_token_logs t + JOIN users u ON u.id = t.user_id + WHERE t.action = 'suspicious' + AND t.created_at >= :start_date + AND t.created_at <= :end_date + ORDER BY t.created_at DESC + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + data = {} + + ipinfo = DiscourseIpInfo.get(row.client_ip) + browser = BrowserDetection.browser(row.user_agent) + device = BrowserDetection.device(row.user_agent) + os = BrowserDetection.os(row.user_agent) + + data[:username] = row.username + data[:user_id] = row.user_id + data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + data[:client_ip] = row.client_ip.to_s + data[:location] = ipinfo[:location] + data[:browser] = I18n.t("user_auth_tokens.browser.#{browser}") + data[:device] = I18n.t("user_auth_tokens.device.#{device}") + data[:os] = I18n.t("user_auth_tokens.os.#{os}") + data[:login_time] = row.login_time + + report.data << data + end +end diff --git a/app/models/reports/system_private_messages.rb b/app/models/reports/system_private_messages.rb new file mode 100644 index 00000000000..98d2dbfa70e --- /dev/null +++ b/app/models/reports/system_private_messages.rb @@ -0,0 +1,4 @@ +Report.add_report("system_private_messages") do |report| + report.icon = 'envelope' + private_messages_report report, TopicSubtype.system_message +end diff --git a/app/models/reports/time_to_first_response.rb b/app/models/reports/time_to_first_response.rb new file mode 100644 index 00000000000..33342b77b5b --- /dev/null +++ b/app/models/reports/time_to_first_response.rb @@ -0,0 +1,11 @@ +Report.add_report("time_to_first_response") do |report| + report.category_filtering = true + report.icon = 'reply' + report.higher_is_better = false + report.data = [] + Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: report.category_id).each do |r| + report.data << { x: r["date"], y: r["hours"].to_f.round(2) } + end + report.total = Topic.time_to_first_response_total(category_id: report.category_id) + report.prev30Days = Topic.time_to_first_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id) +end diff --git a/app/models/reports/top_ignored_users.rb b/app/models/reports/top_ignored_users.rb new file mode 100644 index 00000000000..befb4420cbf --- /dev/null +++ b/app/models/reports/top_ignored_users.rb @@ -0,0 +1,75 @@ +Report.add_report("top_ignored_users") do |report| + report.modes = [:table] + + report.labels = [ + { + type: :user, + properties: { + id: :ignored_user_id, + username: :ignored_username, + avatar: :ignored_user_avatar_template, + }, + title: I18n.t("reports.top_ignored_users.labels.ignored_user") + }, + { + type: :number, + properties: [ + :ignores_count, + ], + title: I18n.t("reports.top_ignored_users.labels.ignores_count") + }, + { + type: :number, + properties: [ + :mutes_count, + ], + title: I18n.t("reports.top_ignored_users.labels.mutes_count") + } + ] + + report.data = [] + + sql = <<~SQL + WITH ignored_users AS ( + SELECT + ignored_user_id as user_id, + COUNT(*) AS ignores_count + FROM ignored_users + WHERE created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' + GROUP BY ignored_user_id + ORDER BY COUNT(*) DESC + LIMIT :limit + ), + muted_users AS ( + SELECT + muted_user_id as user_id, + COUNT(*) AS mutes_count + FROM muted_users + WHERE created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' + GROUP BY muted_user_id + ORDER BY COUNT(*) DESC + LIMIT :limit + ) + + SELECT u.id as user_id, + u.username as username, + u.uploaded_avatar_id as uploaded_avatar_id, + ig.ignores_count as ignores_count, + COALESCE(mu.mutes_count, 0) as mutes_count, + ig.ignores_count + COALESCE(mu.mutes_count, 0) as total + FROM users as u + JOIN ignored_users as ig ON ig.user_id = u.id + LEFT OUTER JOIN muted_users as mu ON mu.user_id = u.id + ORDER BY total DESC + SQL + + DB.query(sql, limit: report.limit || 250).each do |row| + report.data << { + ignored_user_id: row.user_id, + ignored_username: row.username, + ignored_user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + ignores_count: row.ignores_count, + mutes_count: row.mutes_count, + } + end +end diff --git a/app/models/reports/top_referred_topics.rb b/app/models/reports/top_referred_topics.rb new file mode 100644 index 00000000000..635aab136ce --- /dev/null +++ b/app/models/reports/top_referred_topics.rb @@ -0,0 +1,31 @@ +Report.add_report("top_referred_topics") do |report| + report.category_filtering = true + + report.modes = [:table] + + report.labels = [ + { + type: :topic, + properties: { + title: :topic_title, + id: :topic_id + }, + title: I18n.t("reports.top_referred_topics.labels.topic") + }, + { + property: :num_clicks, + type: :number, + title: I18n.t("reports.top_referred_topics.labels.num_clicks") + } + ] + + options = { + end_date: report.end_date, + start_date: report.start_date, + limit: report.limit || 8, + category_id: report.category_id + } + result = nil + result = IncomingLinksReport.find(:top_referred_topics, options) + report.data = result.data +end diff --git a/app/models/reports/top_referrers.rb b/app/models/reports/top_referrers.rb new file mode 100644 index 00000000000..4175cd1dab6 --- /dev/null +++ b/app/models/reports/top_referrers.rb @@ -0,0 +1,34 @@ +Report.add_report("top_referrers") do |report| + report.modes = [:table] + + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :user_avatar_template, + }, + title: I18n.t("reports.top_referrers.labels.user") + }, + { + property: :num_clicks, + type: :number, + title: I18n.t("reports.top_referrers.labels.num_clicks") + }, + { + property: :num_topics, + type: :number, + title: I18n.t("reports.top_referrers.labels.num_topics") + } + ] + + options = { + end_date: report.end_date, + start_date: report.start_date, + limit: report.limit || 8 + } + + result = IncomingLinksReport.find(:top_referrers, options) + report.data = result.data +end diff --git a/app/models/reports/top_traffic_sources.rb b/app/models/reports/top_traffic_sources.rb new file mode 100644 index 00000000000..45c0ee91461 --- /dev/null +++ b/app/models/reports/top_traffic_sources.rb @@ -0,0 +1,32 @@ +Report.add_report("top_traffic_sources") do |report| + report.category_filtering = true + + report.modes = [:table] + + report.labels = [ + { + property: :domain, + title: I18n.t("reports.top_traffic_sources.labels.domain") + }, + { + property: :num_clicks, + type: :number, + title: I18n.t("reports.top_traffic_sources.labels.num_clicks") + }, + { + property: :num_topics, + type: :number, + title: I18n.t("reports.top_traffic_sources.labels.num_topics") + } + ] + + options = { + end_date: report.end_date, + start_date: report.start_date, + limit: report.limit || 8, + category_id: report.category_id + } + + result = IncomingLinksReport.find(:top_traffic_sources, options) + report.data = result.data +end diff --git a/app/models/reports/top_uploads.rb b/app/models/reports/top_uploads.rb new file mode 100644 index 00000000000..f52c9e41a50 --- /dev/null +++ b/app/models/reports/top_uploads.rb @@ -0,0 +1,79 @@ +Report.add_report("top_uploads") do |report| + report.modes = [:table] + + report.filter_options = [ + { + id: "file-extension", + selected: report.filter_values.fetch("file-extension", "any"), + choices: (SiteSetting.authorized_extensions.split("|") + report.filter_values.values).uniq, + allowAny: true + } + ] + + report.labels = [ + { + type: :link, + properties: [ + :file_url, + :file_name, + ], + title: I18n.t("reports.top_uploads.labels.filename") + }, + { + type: :user, + properties: { + username: :author_username, + id: :author_id, + avatar: :author_avatar_template, + }, + title: I18n.t("reports.top_uploads.labels.author") + }, + { + type: :text, + property: :extension, + title: I18n.t("reports.top_uploads.labels.extension") + }, + { + type: :bytes, + property: :filesize, + title: I18n.t("reports.top_uploads.labels.filesize") + }, + ] + + report.data = [] + + sql = <<~SQL + SELECT + u.id as user_id, + u.username, + u.uploaded_avatar_id, + up.filesize, + up.original_filename, + up.extension, + up.url + FROM uploads up + JOIN users u + ON u.id = up.user_id + /*where*/ + ORDER BY up.filesize DESC + LIMIT #{report.limit || 250} + SQL + + extension_filter = report.filter_values["file-extension"] + builder = DB.build(sql) + builder.where("up.id > :seeded_id_threshold", seeded_id_threshold: Upload::SEEDED_ID_THRESHOLD) + builder.where("up.created_at >= :start_date", start_date: report.start_date) + builder.where("up.created_at < :end_date", end_date: report.end_date) + builder.where("up.extension = :extension", extension: extension_filter) if extension_filter.present? + builder.query.each do |row| + data = {} + data[:author_id] = row.user_id + data[:author_username] = row.username + data[:author_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + data[:filesize] = row.filesize + data[:extension] = row.extension + data[:file_url] = Discourse.store.cdn_url(row.url) + data[:file_name] = row.original_filename.truncate(25) + report.data << data + end +end diff --git a/app/models/reports/topics.rb b/app/models/reports/topics.rb new file mode 100644 index 00000000000..851700f18a8 --- /dev/null +++ b/app/models/reports/topics.rb @@ -0,0 +1,7 @@ +Report.add_report("topics") do |report| + report.category_filtering = true + basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id + countable = Topic.listable_topics + countable = countable.in_category_and_subcategories(report.category_id) if report.category_id + add_counts report, countable, 'topics.created_at' +end diff --git a/app/models/reports/topics_with_no_response.rb b/app/models/reports/topics_with_no_response.rb new file mode 100644 index 00000000000..2b1d434db83 --- /dev/null +++ b/app/models/reports/topics_with_no_response.rb @@ -0,0 +1,9 @@ +Report.add_report("topics_with_no_response") do |report| + report.category_filtering = true + report.data = [] + Topic.with_no_response_per_day(report.start_date, report.end_date, report.category_id).each do |r| + report.data << { x: r["date"], y: r["count"].to_i } + end + report.total = Topic.with_no_response_total(category_id: report.category_id) + report.prev30Days = Topic.with_no_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id) +end diff --git a/app/models/reports/trending_search.rb b/app/models/reports/trending_search.rb new file mode 100644 index 00000000000..85b9173e072 --- /dev/null +++ b/app/models/reports/trending_search.rb @@ -0,0 +1,36 @@ +Report.add_report("trending_search") do |report| + report.labels = [ + { + property: :term, + type: :text, + title: I18n.t("reports.trending_search.labels.term") + }, + { + property: :searches, + type: :number, + title: I18n.t("reports.trending_search.labels.searches") + }, + { + type: :percent, + property: :ctr, + title: I18n.t("reports.trending_search.labels.click_through") + } + ] + + report.data = [] + + report.modes = [:table] + + trends = SearchLog.trending_from(report.start_date, + end_date: report.end_date, + limit: report.limit + ) + + trends.each do |trend| + report.data << { + term: trend.term, + searches: trend.searches, + ctr: trend.ctr + } + end +end diff --git a/app/models/reports/user_flagging_ratio.rb b/app/models/reports/user_flagging_ratio.rb new file mode 100644 index 00000000000..bd67590e19f --- /dev/null +++ b/app/models/reports/user_flagging_ratio.rb @@ -0,0 +1,81 @@ +Report.add_report("user_flagging_ratio") do |report| + report.data = [] + + report.modes = [:table] + + report.dates_filtering = true + + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :avatar_template, + }, + title: I18n.t("reports.user_flagging_ratio.labels.user") + }, + { + type: :number, + property: :disagreed_flags, + title: I18n.t("reports.user_flagging_ratio.labels.disagreed_flags") + }, + { + type: :number, + property: :agreed_flags, + title: I18n.t("reports.user_flagging_ratio.labels.agreed_flags") + }, + { + type: :number, + property: :ignored_flags, + title: I18n.t("reports.user_flagging_ratio.labels.ignored_flags") + }, + { + type: :number, + property: :score, + title: I18n.t("reports.user_flagging_ratio.labels.score") + }, + ] + + statuses = ReviewableScore.statuses + + agreed = "SUM(CASE WHEN rs.status = #{statuses[:agreed]} THEN 1 ELSE 0 END)::numeric" + disagreed = "SUM(CASE WHEN rs.status = #{statuses[:disagreed]} THEN 1 ELSE 0 END)::numeric" + ignored = "SUM(CASE WHEN rs.status = #{statuses[:ignored]} THEN 1 ELSE 0 END)::numeric" + + sql = <<~SQL + SELECT u.id, + u.username, + u.uploaded_avatar_id as avatar_id, + CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced, + #{disagreed} AS disagreed_flags, + #{agreed} AS agreed_flags, + #{ignored} AS ignored_flags, + ROUND((1-(#{agreed} / #{disagreed})) * (#{disagreed} - #{agreed})) AS score + FROM users AS u + INNER JOIN reviewable_scores AS rs ON rs.user_id = u.id + WHERE u.id > 0 + AND rs.created_at >= :start_date + AND rs.created_at <= :end_date + GROUP BY u.id, + u.username, + u.uploaded_avatar_id, + u.silenced_till + HAVING #{disagreed} > #{agreed} + ORDER BY score DESC + LIMIT 100 + SQL + + DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| + flagger = {} + flagger[:user_id] = row.id + flagger[:username] = row.username + flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) + flagger[:disagreed_flags] = row.disagreed_flags + flagger[:ignored_flags] = row.ignored_flags + flagger[:agreed_flags] = row.agreed_flags + flagger[:score] = row.score + + report.data << flagger + end +end diff --git a/app/models/reports/user_to_user_private_messages.rb b/app/models/reports/user_to_user_private_messages.rb new file mode 100644 index 00000000000..9fdc8410718 --- /dev/null +++ b/app/models/reports/user_to_user_private_messages.rb @@ -0,0 +1,4 @@ +Report.add_report("user_to_user_private_messages") do |report| + report.icon = 'envelope' + private_messages_report report, TopicSubtype.user_to_user +end diff --git a/app/models/reports/user_to_user_private_messages_with_replies.rb b/app/models/reports/user_to_user_private_messages_with_replies.rb new file mode 100644 index 00000000000..65891e67a87 --- /dev/null +++ b/app/models/reports/user_to_user_private_messages_with_replies.rb @@ -0,0 +1,8 @@ +Report.add_report("user_to_user_private_messages_with_replies") do |report| + report.icon = 'envelope' + topic_subtype = TopicSubtype.user_to_user + subject = Post.where('posts.user_id > 0') + basic_report_about report, subject, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype + subject = Post.private_posts.where('posts.user_id > 0').with_topic_subtype(topic_subtype) + add_counts report, subject, 'posts.created_at' +end diff --git a/app/models/reports/users_by_trust_level.rb b/app/models/reports/users_by_trust_level.rb new file mode 100644 index 00000000000..5252be50559 --- /dev/null +++ b/app/models/reports/users_by_trust_level.rb @@ -0,0 +1,25 @@ +Report.add_report("users_by_trust_level") do |report| + report.data = [] + + report.modes = [:table] + + report.dates_filtering = false + + report.labels = [ + { + property: :key, + title: I18n.t("reports.users_by_trust_level.labels.level") + }, + { + property: :y, + type: :number, + title: I18n.t("reports.default.labels.count") + } + ] + + User.real.group('trust_level').count.sort.each do |level, count| + key = TrustLevel.levels[level.to_i] + url = Proc.new { |k| "/admin/users/list/#{k}" } + report.data << { url: url.call(key), key: key, x: level.to_i, y: count } + end +end diff --git a/app/models/reports/users_by_type.rb b/app/models/reports/users_by_type.rb new file mode 100644 index 00000000000..a14f59804d1 --- /dev/null +++ b/app/models/reports/users_by_type.rb @@ -0,0 +1,34 @@ +Report.add_report("users_by_type") do |report| + report.data = [] + + report.modes = [:table] + + report.dates_filtering = false + + report.labels = [ + { + property: :x, + title: I18n.t("reports.users_by_type.labels.type") + }, + { + property: :y, + type: :number, + title: I18n.t("reports.default.labels.count") + } + ] + + label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") } + url = Proc.new { |key| "/admin/users/list/#{key}" } + + admins = User.real.admins.count + report.data << { url: url.call("admins"), icon: "shield-alt", key: "admins", x: label.call("admin"), y: admins } if admins > 0 + + moderators = User.real.moderators.count + report.data << { url: url.call("moderators"), icon: "shield-alt", key: "moderators", x: label.call("moderator"), y: moderators } if moderators > 0 + + suspended = User.real.suspended.count + report.data << { url: url.call("suspended"), icon: "ban", key: "suspended", x: label.call("suspended"), y: suspended } if suspended > 0 + + silenced = User.real.silenced.count + report.data << { url: url.call("silenced"), icon: "ban", key: "silenced", x: label.call("silenced"), y: silenced } if silenced > 0 +end diff --git a/app/models/reports/visits.rb b/app/models/reports/visits.rb new file mode 100644 index 00000000000..4d95537c194 --- /dev/null +++ b/app/models/reports/visits.rb @@ -0,0 +1,9 @@ +Report.add_report("visits") do |report| + report.group_filtering = true + report.icon = 'user' + + basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id + add_counts report, UserVisit, 'visited_at' + + report.prev30Days = UserVisit.where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count +end diff --git a/app/models/reports/visits_mobile.rb b/app/models/reports/visits_mobile.rb new file mode 100644 index 00000000000..c31adbe3010 --- /dev/null +++ b/app/models/reports/visits_mobile.rb @@ -0,0 +1,5 @@ +Report.add_report("mobile_visits") do |report| + basic_report_about report, UserVisit, :mobile_by_day, report.start_date, report.end_date + report.total = UserVisit.where(mobile: true).count + report.prev30Days = UserVisit.where(mobile: true).where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count +end