canvas-lms/app/models/content_participation_count.rb

133 lines
4.9 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2012 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class ContentParticipationCount < ActiveRecord::Base
ACCESSIBLE_ATTRIBUTES = [:context, :user, :content_type, :unread_count].freeze
belongs_to :context, polymorphic: [:course]
belongs_to :user
before_create :set_root_account_id
def self.create_or_update(opts={})
opts = opts.with_indifferent_access
context = opts.delete(:context)
user = opts.delete(:user)
type = opts.delete(:content_type)
return nil unless user && context
participant = nil
context.shard.activate do
unique_constraint_retry do
participant = context.content_participation_counts.where(:user_id => user, :content_type => type).lock.first
if participant.blank?
participant ||= context.content_participation_counts.build({
:user => user,
:content_type => type,
:unread_count => unread_count_for(type, context, user),
})
end
participant.attributes = opts.slice(*ACCESSIBLE_ATTRIBUTES)
# if the participant was just created, the count will already be correct
if opts[:offset].present? && !participant.new_record?
participant.unread_count = participant.unread_count(!:refresh) + opts[:offset]
end
participant.save if participant.new_record? || participant.changed?
end
end
participant
end
def self.unread_count_for(type, context, user)
return 0 unless user.present? && context.present?
case type
when "Submission"
self.unread_submission_count_for(context, user)
else
0
end
end
def self.unread_submission_count_for(context, user)
return 0 unless context.is_a?(Course) && context.user_is_student?(user)
GuardRail.activate(:secondary) do
potential_ids = Rails.cache.fetch_with_batched_keys(["potential_unread_submission_ids", context.global_id].cache_key,
batch_object: user, batched_keys: :submissions) do
submission_conditions = sanitize_sql_for_conditions([<<~SQL, user.id, context.class.to_s, context.id])
submissions.user_id = ? AND
assignments.context_type = ? AND
assignments.context_id = ? AND
assignments.workflow_state NOT IN ('deleted', 'unpublished') AND
assignments.submission_types != 'not_graded' AND
(assignments.muted IS NULL OR NOT assignments.muted)
SQL
subs_with_grades = Submission.active.graded.
joins(:assignment).
where(submission_conditions).
where("submissions.score IS NOT NULL").
pluck(:id)
subs_with_comments = Submission.active.
joins(:assignment, :submission_comments).
where(submission_conditions).
where(<<~SQL, user).pluck(:id)
(submission_comments.hidden IS NULL OR NOT submission_comments.hidden)
AND NOT submission_comments.draft
AND submission_comments.provisional_grade_id IS NULL
AND submission_comments.author_id <> ?
SQL
(subs_with_grades + subs_with_comments).uniq
end
already_read_count = potential_ids.any? ? ContentParticipation.where(
:content_type => "Submission",
:content_id => potential_ids,
:user_id => user,
:workflow_state => "read"
).count : 0
potential_ids.size - already_read_count
end
end
def unread_count(refresh = true)
refresh_unread_count if refresh && !frozen? && ttl.present? && self.updated_at.utc < ttl.seconds.ago.utc
read_attribute(:unread_count)
end
def refresh_unread_count
self.unread_count = ContentParticipationCount.unread_count_for(content_type, context, user)
GuardRail.activate(:primary) {self.save} if self.changed?
end
def set_root_account_id
self.root_account_id = self.context&.root_account_id
end
# Things we know of that will only get updated by a refresh:
# - delayed_post announcements
# - unlocking discussions/announcements from a module
# - unmuting an assignment with submissions
# - deleting a discussion/announcement/assignment/submission
# - marking a previously graded assignment as not_graded
def ttl
Setting.get('content_participation_count_ttl', 30.minutes).to_i
end
private :ttl
end