Class: Report

Inherits:
Object
  • Object
show all
Includes:
Reports::Bookmarks, Reports::ConsolidatedApiRequests, Reports::ConsolidatedPageViews, Reports::DailyEngagedUsers, Reports::DauByMau, Reports::Emails, Reports::Flags, Reports::FlagsStatus, Reports::Likes, Reports::MobileVisits, Reports::ModeratorWarningPrivateMessages, Reports::ModeratorsActivity, Reports::NewContributors, Reports::NotifyModeratorsPrivateMessages, Reports::NotifyUserPrivateMessages, Reports::PostEdits, Reports::Posts, Reports::ProfileViews, Reports::Signups, Reports::StaffLogins, Reports::StorageStats, Reports::SuspiciousLogins, Reports::SystemPrivateMessages, Reports::TimeToFirstResponse, Reports::TopIgnoredUsers, Reports::TopReferredTopics, Reports::TopReferrers, Reports::TopTrafficSources, Reports::TopUploads, Reports::TopUsersByLikesReceived, Reports::TopUsersByLikesReceivedFromAVarietyOfPeople, Reports::TopUsersByLikesReceivedFromInferiorTrustLevel, Reports::Topics, Reports::TopicsWithNoResponse, Reports::TrendingSearch, Reports::TrustLevelGrowth, Reports::UserFlaggingRatio, Reports::UserToUserPrivateMessages, Reports::UserToUserPrivateMessagesWithReplies, Reports::UsersByTrustLevel, Reports::UsersByType, Reports::Visits, Reports::WebCrawlers
Defined in:
app/models/report.rb

Constant Summary collapse

SCHEMA_VERSION =

Change this line each time report format change and you want to ensure cache is reset

4
FILTERS =
%i[
  name
  start_date
  end_date
  category
  group
  trust_level
  file_extension
  include_subcategories
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type) ⇒ Report

Returns a new instance of Report.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/models/report.rb', line 99

def initialize(type)
  @type = type
  @start_date ||= Report.default_days.days.ago.utc.beginning_of_day
  @end_date ||= Time.now.utc.end_of_day
  @prev_end_date = @start_date
  @average = false
  @percent = false
  @higher_is_better = true
  @modes = %i[table chart]
  @prev_data = nil
  @dates_filtering = true
  @available_filters = {}
  @filters = {}

  tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc"
  @primary_color = rgba_color(tertiary)
  @secondary_color = rgba_color(tertiary, 0.1)
end

Instance Attribute Details

#available_filtersObject

Returns the value of attribute available_filters.



63
64
65
# File 'app/models/report.rb', line 63

def available_filters
  @available_filters
end

#averageObject

Returns the value of attribute average.



63
64
65
# File 'app/models/report.rb', line 63

def average
  @average
end

#dataObject

Returns the value of attribute data.



63
64
65
# File 'app/models/report.rb', line 63

def data
  @data
end

#dates_filteringObject

Returns the value of attribute dates_filtering.



63
64
65
# File 'app/models/report.rb', line 63

def dates_filtering
  @dates_filtering
end

#end_dateObject

Returns the value of attribute end_date.



63
64
65
# File 'app/models/report.rb', line 63

def end_date
  @end_date
end

#errorObject

Returns the value of attribute error.



63
64
65
# File 'app/models/report.rb', line 63

def error
  @error
end

#facetsObject

Returns the value of attribute facets.



63
64
65
# File 'app/models/report.rb', line 63

def facets
  @facets
end

#filtersObject

Returns the value of attribute filters.



63
64
65
# File 'app/models/report.rb', line 63

def filters
  @filters
end

#higher_is_betterObject

Returns the value of attribute higher_is_better.



63
64
65
# File 'app/models/report.rb', line 63

def higher_is_better
  @higher_is_better
end

#iconObject

Returns the value of attribute icon.



63
64
65
# File 'app/models/report.rb', line 63

def icon
  @icon
end

#labelsObject

Returns the value of attribute labels.



63
64
65
# File 'app/models/report.rb', line 63

def labels
  @labels
end

#limitObject

Returns the value of attribute limit.



63
64
65
# File 'app/models/report.rb', line 63

def limit
  @limit
end

#modesObject

Returns the value of attribute modes.



63
64
65
# File 'app/models/report.rb', line 63

def modes
  @modes
end

#percentObject

Returns the value of attribute percent.



63
64
65
# File 'app/models/report.rb', line 63

def percent
  @percent
end

#prev30DaysObject

Returns the value of attribute prev30Days.



63
64
65
# File 'app/models/report.rb', line 63

def prev30Days
  @prev30Days
end

#prev_dataObject

Returns the value of attribute prev_data.



63
64
65
# File 'app/models/report.rb', line 63

def prev_data
  @prev_data
end

#prev_end_dateObject

Returns the value of attribute prev_end_date.



63
64
65
# File 'app/models/report.rb', line 63

def prev_end_date
  @prev_end_date
end

#prev_periodObject

Returns the value of attribute prev_period.



63
64
65
# File 'app/models/report.rb', line 63

def prev_period
  @prev_period
end

#prev_start_dateObject

Returns the value of attribute prev_start_date.



63
64
65
# File 'app/models/report.rb', line 63

def prev_start_date
  @prev_start_date
end

#primary_colorObject

Returns the value of attribute primary_color.



63
64
65
# File 'app/models/report.rb', line 63

def primary_color
  @primary_color
end

#secondary_colorObject

Returns the value of attribute secondary_color.



63
64
65
# File 'app/models/report.rb', line 63

def secondary_color
  @secondary_color
end

#start_dateObject

Returns the value of attribute start_date.



63
64
65
# File 'app/models/report.rb', line 63

def start_date
  @start_date
end

#totalObject

Returns the value of attribute total.



63
64
65
# File 'app/models/report.rb', line 63

def total
  @total
end

#typeObject

Returns the value of attribute type.



63
64
65
# File 'app/models/report.rb', line 63

def type
  @type
end

Class Method Details

._get(type, opts = nil) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'app/models/report.rb', line 225

def self._get(type, opts = nil)
  opts ||= {}

  # Load the report
  report = Report.new(type)
  report.start_date = opts[:start_date] if opts[:start_date]
  report.end_date = opts[:end_date] if opts[:end_date]
  report.facets = opts[:facets] || %i[total prev30Days]
  report.limit = opts[:limit] if opts[:limit]
  report.average = opts[:average] if opts[:average]
  report.percent = opts[:percent] if opts[:percent]
  report.filters = opts[:filters] if opts[:filters]
  report.labels = Report.default_labels

  report
end

.add_counts(report, subject_class, query_column = "created_at") ⇒ Object



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'app/models/report.rb', line 350

def self.add_counts(report, subject_class, query_column = "created_at")
  if report.facets.include?(:prev_period)
    prev_data =
      subject_class.where(
        "#{query_column} >= ? and #{query_column} < ?",
        report.prev_start_date,
        report.prev_end_date,
      )

    report.prev_period = prev_data.count
  end

  report.total = subject_class.count if report.facets.include?(:total)

  if report.facets.include?(:prev30Days)
    report.prev30Days =
      subject_class.where(
        "#{query_column} >= ? and #{query_column} < ?",
        report.start_date - 30.days,
        report.start_date,
      ).count
  end
end

.add_prev_data(report, subject_class, report_method, *args) ⇒ Object



343
344
345
346
347
348
# File 'app/models/report.rb', line 343

def self.add_prev_data(report, subject_class, report_method, *args)
  if report.modes.include?(:chart) && report.facets.include?(:prev_period)
    prev_data = subject_class.public_send(report_method, *args)
    report.prev_data = prev_data.map { |k, v| { x: k, y: v } }
  end
end

.add_report(name, &block) ⇒ Object



221
222
223
# File 'app/models/report.rb', line 221

def Report.add_report(name, &block)
  singleton_class.instance_eval { define_method("report_#{name}", &block) }
end

.basic_report_about(report, subject_class, report_method, *args) ⇒ Object



335
336
337
338
339
340
341
# File 'app/models/report.rb', line 335

def self.basic_report_about(report, subject_class, report_method, *args)
  report.data = []

  subject_class
    .public_send(report_method, *args)
    .each { |date, count| report.data << { x: date, y: count } }
end

.cache(report) ⇒ Object



247
248
249
250
# File 'app/models/report.rb', line 247

def self.cache(report)
  duration = report.error == :exception ? 1.minute : 35.minutes
  Discourse.cache.write(cache_key(report), report.as_json, expires_in: duration)
end

.cache_key(report) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/models/report.rb', line 118

def self.cache_key(report)
  [
    "reports",
    report.type,
    report.start_date.to_date.strftime("%Y%m%d"),
    report.end_date.to_date.strftime("%Y%m%d"),
    report.facets,
    report.limit,
    report.filters.blank? ? nil : MultiJson.dump(report.filters),
    SCHEMA_VERSION,
  ].compact.map(&:to_s).join(":")
end

.clear_cache(type = nil) ⇒ Object



151
152
153
154
155
# File 'app/models/report.rb', line 151

def self.clear_cache(type = nil)
  pattern = type ? "reports:#{type}:*" : "reports:*"

  Discourse.cache.keys(pattern).each { |key| Discourse.cache.redis.del(key) }
end

.default_daysObject



88
89
90
# File 'app/models/report.rb', line 88

def self.default_days
  30
end

.default_labelsObject



92
93
94
95
96
97
# File 'app/models/report.rb', line 92

def self.default_labels
  [
    { type: :date, property: :x, title: I18n.t("reports.default.labels.day") },
    { type: :number, property: :y, title: I18n.t("reports.default.labels.count") },
  ]
end

.find(type, opts = nil) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'app/models/report.rb', line 252

def self.find(type, opts = nil)
  opts ||= {}

  begin
    report = _get(type, opts)
    report_method = :"report_#{type}"

    begin
      wrap_slow_query do
        if respond_to?(report_method)
          public_send(report_method, report)
        elsif type =~ /_reqs\z/
          req_report(report, type.split(/_reqs\z/)[0].to_sym)
        else
          return nil
        end
      end
    rescue ActiveRecord::QueryCanceled, PG::QueryCanceled => e
      report.error = :timeout
    end
  rescue Exception => e
    # In test mode, don't swallow exceptions by default to help debug errors.
    raise if Rails.env.test? && !opts[:wrap_exceptions_in_test]

    # ensures that if anything unexpected prevents us from
    # creating a report object we fail elegantly and log an error
    if !report
      Rails.logger.error("Couldn’t create report `#{type}`: <#{e.class} #{e.message}>")
      return nil
    end

    report.error = :exception

    # given reports can be added by plugins we don’t want dashboard failures
    # on report computation, however we do want to log which report is provoking
    # an error
    Rails.logger.error(
      "Error while computing report `#{report.type}`: #{e.message}\n#{e.backtrace.join("\n")}",
    )
  end

  report
end

.find_cached(type, opts = nil) ⇒ Object



242
243
244
245
# File 'app/models/report.rb', line 242

def self.find_cached(type, opts = nil)
  report = _get(type, opts)
  Discourse.cache.read(cache_key(report))
end

.post_action_report(report, post_action_type) ⇒ Object



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'app/models/report.rb', line 374

def self.post_action_report(report, post_action_type)
  category_id, include_subcategories = report.add_category_filter

  report.data = []
  PostAction
    .count_per_day_for_type(
      post_action_type,
      category_id: category_id,
      include_subcategories: include_subcategories,
      start_date: report.start_date,
      end_date: report.end_date,
    )
    .each { |date, count| report.data << { x: date, y: count } }

  countable = PostAction.unscoped.where(post_action_type_id: post_action_type)
  if category_id
    if include_subcategories
      countable =
        countable.joins(post: :topic).where(
          "topics.category_id IN (?)",
          Category.subcategory_ids(category_id),
        )
    else
      countable = countable.joins(post: :topic).where("topics.category_id = ?", category_id)
    end
  end

  add_counts report, countable, "post_actions.created_at"
end

.private_messages_report(report, topic_subtype) ⇒ Object



404
405
406
407
408
409
410
411
412
413
414
415
# File 'app/models/report.rb', line 404

def self.private_messages_report(report, topic_subtype)
  report.icon = "envelope"
  subject = Topic.where("topics.user_id > 0")
  basic_report_about report,
                     subject,
                     :private_message_topics_count_per_day,
                     report.start_date,
                     report.end_date,
                     topic_subtype
  subject = Topic.private_messages.where("topics.user_id > 0").with_subtype(topic_subtype)
  add_counts report, subject, "topics.created_at"
end

.report_about(report, subject_class, report_method = :count_per_day) ⇒ Object



330
331
332
333
# File 'app/models/report.rb', line 330

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
end

.req_report(report, filter = nil) ⇒ Object



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'app/models/report.rb', line 296

def self.req_report(report, filter = nil)
  data =
    if filter == :page_view_total
      ApplicationRequest.where(
        req_type: [
          ApplicationRequest
            .req_types
            .reject { |k, v| k =~ /mobile/ }
            .map { |k, v| v if k =~ /page_view/ }
            .compact,
        ].flatten,
      )
    else
      ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
    end

  report.icon = "file" if filter == :page_view_total

  report.data = []
  data
    .where("date >= ? AND date <= ?", report.start_date, report.end_date)
    .order(date: :asc)
    .group(:date)
    .sum(:count)
    .each { |date, count| report.data << { x: date, y: count } }

  report.total = data.sum(:count)

  report.prev30Days =
    data.where("date >= ? AND date < ?", (report.start_date - 31.days), report.start_date).sum(
      :count,
    )
end

.wrap_slow_query(timeout = 20_000) ⇒ Object



157
158
159
160
161
162
163
164
165
# File 'app/models/report.rb', line 157

def self.wrap_slow_query(timeout = 20_000)
  ActiveRecord::Base.connection.transaction do
    # Allows only read only transactions
    DB.exec "SET TRANSACTION READ ONLY"
    # Set a statement timeout so we can't tie up the server
    DB.exec "SET LOCAL statement_timeout = #{timeout}"
    yield
  end
end

Instance Method Details

#add_category_filterObject



139
140
141
142
143
144
145
146
147
148
149
# File 'app/models/report.rb', line 139

def add_category_filter
  category_id = filters[:category].to_i if filters[:category].present?
  add_filter("category", type: "category", default: category_id)
  return if category_id.blank?

  include_subcategories = filters[:include_subcategories]
  include_subcategories = !!ActiveRecord::Type::Boolean.new.cast(include_subcategories)
  add_filter("include_subcategories", type: "bool", default: include_subcategories)

  [category_id, include_subcategories]
end

#add_filter(name, options = {}) ⇒ Object



131
132
133
# File 'app/models/report.rb', line 131

def add_filter(name, options = {})
  available_filters[name] = options
end

#as_json(options = nil) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'app/models/report.rb', line 175

def as_json(options = nil)
  description = I18n.t("reports.#{type}.description", default: "")
  description_link = I18n.t("reports.#{type}.description_link", default: "")

  {
    type: type,
    title: I18n.t("reports.#{type}.title", default: nil),
    xaxis: I18n.t("reports.#{type}.xaxis", default: nil),
    yaxis: I18n.t("reports.#{type}.yaxis", default: nil),
    description: description.presence ? description : nil,
    description_link: description_link.presence ? description_link : nil,
    data: data,
    start_date: start_date&.iso8601,
    end_date: end_date&.iso8601,
    prev_data: self.prev_data,
    prev_start_date: prev_start_date&.iso8601,
    prev_end_date: prev_end_date&.iso8601,
    prev30Days: self.prev30Days,
    dates_filtering: self.dates_filtering,
    report_key: Report.cache_key(self),
    primary_color: self.primary_color,
    secondary_color: self.secondary_color,
    available_filters: self.available_filters.map { |k, v| { id: k }.merge(v) },
    labels: labels || Report.default_labels,
    average: self.average,
    percent: self.percent,
    higher_is_better: self.higher_is_better,
    modes: self.modes,
  }.tap do |json|
    json[:icon] = self.icon if self.icon
    json[:error] = self.error if self.error
    json[:total] = self.total if self.total
    json[:prev_period] = self.prev_period if self.prev_period
    json[:prev30Days] = self.prev30Days if self.prev30Days
    json[:limit] = self.limit if self.limit

    if type == "page_view_crawler_reqs"
      json[:related_report] = Report.find(
        "web_crawlers",
        start_date: start_date,
        end_date: end_date,
      )&.as_json
    end
  end
end

#colorsObject



422
423
424
# File 'app/models/report.rb', line 422

def colors
  %w[#1EB8D1 #9BC53D #721D8D #E84A5F #8A6916]
end

#remove_filter(name) ⇒ Object



135
136
137
# File 'app/models/report.rb', line 135

def remove_filter(name)
  available_filters.delete(name)
end

#rgba_color(hex, opacity = 1) ⇒ Object



417
418
419
420
# File 'app/models/report.rb', line 417

def rgba_color(hex, opacity = 1)
  rgbs = hex_to_rgbs(adjust_hex(hex))
  "rgba(#{rgbs.join(",")},#{opacity})"
end