<commit_before>#
# Copyright (C) 2013 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/>.
#

require_dependency 'messageable_user'

class MessageableUser
  class Calculator
    CONTEXT_RECIPIENT = /\A(course|section|group)_(\d+)(_([a-z]+))?\z/
    INDIVIDUAL_RECIPIENT = /\A\d+\z/

    # all work is done within the context of a user. avoid passing it around in
    # every single method call by being an object instead of just a collection
    # of class methods
    def initialize(user)
      @user = user
    end

    # convenience module versions of the primary three methods
    def self.load_messageable_users(viewing_user, target_users, options={})
      new(viewing_user).load_messageable_users(target_users, options)
    end

    def self.load_messageable_user(viewing_user, target_user, options={})
      new(viewing_user).load_messageable_user(target_user, options)
    end

    def self.messageable_users_in_context(viewing_user, asset_string)
      new(viewing_user).messageable_users_in_context(asset_string)
    end

    # Takes a list of Users (or ids, or MessageableUsers) and:
    #
    #  * turns them into MessageableUsers if they weren't already
    #  * bulk loads any common contexts for those MessageableUsers
    #  * filters them to just those actually messageable by the viewing user
    #
    # with the :strict_checks option false (default: true), all provided users
    # will be considered messageable and enrollments in unpublished courses,
    # etc. will be included when determining common contexts. should be used
    # only when loading contexts for users already in a conversation
    #
    # the :admin_context option allows specifying a context (course, section,
    # or group) to look for users in. the viewing user will be treated as
    # having full visibility in that context for the purpose of checking
    # messageability and loading common contexts.
    #
    # the :conversation_id option allows specifying a conversation to look
    # for users in. ignored if the viewing user is not already be a participant
    # in the conversation. any other user participating in that conversation is
    # considered messageable.
    def load_messageable_users(users, options={})
      strict_checks = {:strict_checks => true}.merge(options)[:strict_checks]

      # we can be given user ids, Users (from which we just use the ids), or
      # MessageableUsers. if they're not already MessageableUsers, make them so
      # (and check availability while at it, unless we're skipping that)
      return [] unless users.present?
      unless users.first.is_a?(MessageableUser)
        user_ids = users.first.is_a?(User) ? users.map(&:id) : users
        users = Shard.partition_by_shard(user_ids) do |shard_user_ids|
          MessageableUser.prepped(:strict_checks => strict_checks).
            where(:id => shard_user_ids).all
        end
        return [] unless users
      end

      # interpret admin context, if any
      case options[:admin_context]
      when Course then include_course_id = options[:admin_context].id
      when CourseSection then include_course_id = options[:admin_context].course_id
      when Group then include_group_id = options[:admin_context].id
      end

      # what common courses and groups do they have, if any. if they had some,
      # they're definitely messageable
      other_users = users.reject{ |u| u.id == @user.id }
      if other_users.present?
        load_common_courses_with_users(other_users, include_course_id, strict_checks)
        load_common_groups_with_users(other_users, include_group_id, strict_checks)
      end

      # keep only the ones that look messageable (have a shared context, are
      # self, or share the given conversation)
      if strict_checks
        questionable = users.select do |user|
          !user.common_courses.present? &&
          !user.common_groups.present? &&
          user.id != @user.id
        end

        if questionable.present? && options[:conversation_id].present?
          participants = participants_in_conversation([@user, *questionable], options[:conversation_id].to_i)
          if participants.detect{ |user| user == @user }
            questionable -= participants
          end
        end

        users -= questionable
      end

      users
    end

    # convenience method. as load_messageable_users, but for a single user
    def load_messageable_user(user, options={})
      load_messageable_users([user], options).first
    end

    # find and return all the messageable users in a particular context,
    # specified by an extended asset string (TODO: legacy, should improve
    # interface in the future)
    #
    # the string should be something like 'group_123', 'section_123_students',
    # 'course_123_admins', etc. the third portion of the asset string specifies
    # the types of enrollments to include for course or section contexts; it is
    # ignored for group contexts. the 'admins' enrollment type refers to
    # teachers and tas.
    #
    # NOTE: the common_courses and common_groups of the returned
    # MessageableUser objects will be populated only with the context given.
    # this is by design, and desired behavior for the use case this method is
    # directed at. it may be confusing, however, to those not expecting it.
    def messageable_users_in_context(asset_string)
      return [] unless asset_string.sub(/_all\z/, '') =~ CONTEXT_RECIPIENT

      context_type = $1
      context_id = $2.to_i
      enrollment_type = $4
      if enrollment_type == 'admins'
        enrollment_types = ['TeacherEnrollment','TaEnrollment']
      elsif enrollment_type
        enrollment_types = ["#{enrollment_type.capitalize.singularize}Enrollment"]
      end

      case context_type
      when 'course' then messageable_users_in_course(context_id, enrollment_types)
      when 'section' then messageable_users_in_section(context_id, enrollment_types)
      when 'group' then messageable_users_in_group(context_id)
      else []
      end
    end

    # ==========================  end of public API  ==========================
    # |                                                                       |
    # |  the rest of these methods are to simplify the implementation of the  |
    # |  above, and increase testability. they're not private so that they    |
    # |  can be tested in isolation, but should not be used directly          |
    # |                                                                       |
    # =========================================================================

    # given a list of MessageableUsers, loads the common courses between each
    # of those users and the viewing user into the respective MessageableUser
    # objects.
    #
    # the optional include_course_id lets you treat a specific course as if the
    # viewing user had full visibility in it. (TODO: is this right when
    # include_course_id came from a CourseSection admin_context?)
    #
    # the optional strict_checks parameter (default: true) is passed down to
    # the queried scopes (see enrollment_scope and account_user_scope).
    def load_common_courses_with_users(users, include_course_id=nil, strict_checks=true)
      # with messageability constraints, do I see any of the users in any of my visible
      # courses, and if so with which enrollment type(s)?
      Shard.with_each_shard(@user.associated_shards) do
        if all_courses.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          visible_enrollment_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
          end
        end
      end

      # skipping messageability constraints, do I see the user in that specific
      # course, and if so with which enrollment type(s)?
      if include_course_id && course = Course.find_by_id(include_course_id)
        missing_users = users.reject{ |user| user.global_common_courses.keys.include?(course.global_id) }
        if missing_user.present?
          course.shard.activate do
            reverse_lookup = missing_users.index_by(&:id)
            missing_user_ids = reverse_lookup.keys
            enrollment_scope(:strict_checks => strict_checks).where(:id => missing_user_ids, 'courses.id' => course.id).each do |user|
              reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
            end
          end
        end
      end

      # do I see the user in any of the accounts I admin, and if so with what
      # primary enrollment type?
      Shard.with_each_shard(@user.associated_shards) do
        if visible_account_ids.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          visible_account_user_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
          end
        end
      end
    end

    # given a list of MessageableUsers, loads the common groups between each
    # of those users and the viewing user into the respective MessageableUser
    # objects.
    #
    # the optional include_group_id lets you treat a specific group as if the
    # viewing user had full visibility in it.
    #
    # the optional strict_checks parameter (default: true) is passed down to
    # the queried scopes (see group_user_scope).
    def load_common_groups_with_users(users, include_group_id=nil, strict_checks=true)
      # with messageability constraints, do I see the user in any of my visible
      # groups?
      Shard.with_each_shard(@user.associated_shards) do
        if fully_visible_group_ids.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          fully_visible_group_user_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_groups.merge!(user.global_common_groups)
          end
        end
      end

      # skipping messageability constraints, do I see the user in that specific
      # group?
      if include_group_id && group = Group.find_by_id(include_group_id)
        missing_users = users.reject{ |user| user.global_common_groups.keys.include?(group.global_id) }
        if missing_users.present?
          group.shard.activate do
            reverse_lookup = missing_users.index_by(&:id)
            missing_user_ids = reverse_lookup.keys
            group_user_scope(:strict_checks => strict_checks).where(:id => missing_user_ids, 'group_memberships.group_id' => group.id).each do |user|
              reverse_lookup[user.id].global_common_groups.merge!(user.global_common_groups)
            end
          end
        end
      end
    end

    # filters the provided list of users to just those that are participants in
    # the conversation
    def participants_in_conversation(users, conversation_id)
      conversation = Conversation.find_by_id(conversation_id)
      return [] unless conversation

      conversation.shard.activate do
        reverse_lookup = users.index_by(&:id)
        user_ids = reverse_lookup.keys
        ConversationParticipant.where(
          :user_id => user_ids,
          :conversation_id => conversation_id
        ).all(:select => 'user_id').map{ |cp| reverse_lookup[cp.user_id] }
      end
    end

    # find and return all the messageable users in a particular course (see
    # messageable_users_in_context). the optional enrollment_type restricts to
    # just users with those enrollments
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the course given.
    def messageable_users_in_course(course_id, enrollment_types=nil)
      course = Course.find_by_id(course_id)
      return [] unless course

      course.shard.activate do
        # make sure the course is recognized
        return [] unless course = course_index[course.id]

        scope =
          case course_visibility(course)
          when :full then enrollment_scope.where(full_visibility_clause([course]))
          when :sections then enrollment_scope.where(section_visibility_clause([course]))
          when :restricted then enrollment_scope.where(restricted_visibility_clause([course]))
          end
        scope = scope.where(observer_restriction_clause) if student_courses.present?
        scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
        scope.all
      end
    end

    # find and return all the messageable users in a particular course section
    # (see messageable_users_in_context). the optional enrollment_type
    # restricts to just users with those enrollments
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the course of the given section.
    def messageable_users_in_section(section_id, enrollment_types=nil)
      section = CourseSection.find_by_id(section_id)
      return [] unless section

      section.shard.activate do
        return [] unless course = course_index[section.course_id]
        return [] unless
          fully_visible_courses.include?(course) ||
          section_visible_courses.include?(course) &&
          visible_section_ids_in_courses([course]).include?(section_id)

        scope = enrollment_scope.where('enrollments.course_section_id' => section_id)
        scope = scope.where(observer_restriction_clause) if student_courses.present?
        scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
        scope.all
      end
    end

    # find and return all the messageable users in a particular group (see
    # messageable_users_in_context).
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the group given.
    def messageable_users_in_group(group_id)
      group = Group.find_by_id(group_id)
      return [] unless group

      group.shard.activate do
        if fully_visible_group_ids.include?(group.id)
          group_user_scope.where('group_memberships.group_id' => group.id).all
        elsif section_visible_group_ids.include?(group.id)
          # group.context is guaranteed to be a course from
          # section_visible_courses at this point
          course = course_index[group.context_id]
          scope = enrollment_scope(:common_group_column => group.id, :include_completed => false).where([
            "course_section_id IN (?) AND EXISTS(SELECT 1 FROM group_memberships WHERE user_id=users.id AND group_id=?)",
            visible_section_ids_in_courses([course]), group.id])
          scope = scope.where(observer_restriction_clause) if student_courses.present?
          scope.all
        else
          return []<commit_msg>don't include concluded enrollments when messaging a course<commit_after>#
# Copyright (C) 2013 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/>.
#

require_dependency 'messageable_user'

class MessageableUser
  class Calculator
    CONTEXT_RECIPIENT = /\A(course|section|group)_(\d+)(_([a-z]+))?\z/
    INDIVIDUAL_RECIPIENT = /\A\d+\z/

    # all work is done within the context of a user. avoid passing it around in
    # every single method call by being an object instead of just a collection
    # of class methods
    def initialize(user)
      @user = user
    end

    # convenience module versions of the primary three methods
    def self.load_messageable_users(viewing_user, target_users, options={})
      new(viewing_user).load_messageable_users(target_users, options)
    end

    def self.load_messageable_user(viewing_user, target_user, options={})
      new(viewing_user).load_messageable_user(target_user, options)
    end

    def self.messageable_users_in_context(viewing_user, asset_string)
      new(viewing_user).messageable_users_in_context(asset_string)
    end

    # Takes a list of Users (or ids, or MessageableUsers) and:
    #
    #  * turns them into MessageableUsers if they weren't already
    #  * bulk loads any common contexts for those MessageableUsers
    #  * filters them to just those actually messageable by the viewing user
    #
    # with the :strict_checks option false (default: true), all provided users
    # will be considered messageable and enrollments in unpublished courses,
    # etc. will be included when determining common contexts. should be used
    # only when loading contexts for users already in a conversation
    #
    # the :admin_context option allows specifying a context (course, section,
    # or group) to look for users in. the viewing user will be treated as
    # having full visibility in that context for the purpose of checking
    # messageability and loading common contexts.
    #
    # the :conversation_id option allows specifying a conversation to look
    # for users in. ignored if the viewing user is not already be a participant
    # in the conversation. any other user participating in that conversation is
    # considered messageable.
    def load_messageable_users(users, options={})
      strict_checks = {:strict_checks => true}.merge(options)[:strict_checks]

      # we can be given user ids, Users (from which we just use the ids), or
      # MessageableUsers. if they're not already MessageableUsers, make them so
      # (and check availability while at it, unless we're skipping that)
      return [] unless users.present?
      unless users.first.is_a?(MessageableUser)
        user_ids = users.first.is_a?(User) ? users.map(&:id) : users
        users = Shard.partition_by_shard(user_ids) do |shard_user_ids|
          MessageableUser.prepped(:strict_checks => strict_checks).
            where(:id => shard_user_ids).all
        end
        return [] unless users
      end

      # interpret admin context, if any
      case options[:admin_context]
      when Course then include_course_id = options[:admin_context].id
      when CourseSection then include_course_id = options[:admin_context].course_id
      when Group then include_group_id = options[:admin_context].id
      end

      # what common courses and groups do they have, if any. if they had some,
      # they're definitely messageable
      other_users = users.reject{ |u| u.id == @user.id }
      if other_users.present?
        load_common_courses_with_users(other_users, include_course_id, strict_checks)
        load_common_groups_with_users(other_users, include_group_id, strict_checks)
      end

      # keep only the ones that look messageable (have a shared context, are
      # self, or share the given conversation)
      if strict_checks
        questionable = users.select do |user|
          !user.common_courses.present? &&
          !user.common_groups.present? &&
          user.id != @user.id
        end

        if questionable.present? && options[:conversation_id].present?
          participants = participants_in_conversation([@user, *questionable], options[:conversation_id].to_i)
          if participants.detect{ |user| user == @user }
            questionable -= participants
          end
        end

        users -= questionable
      end

      users
    end

    # convenience method. as load_messageable_users, but for a single user
    def load_messageable_user(user, options={})
      load_messageable_users([user], options).first
    end

    # find and return all the messageable users in a particular context,
    # specified by an extended asset string (TODO: legacy, should improve
    # interface in the future)
    #
    # the string should be something like 'group_123', 'section_123_students',
    # 'course_123_admins', etc. the third portion of the asset string specifies
    # the types of enrollments to include for course or section contexts; it is
    # ignored for group contexts. the 'admins' enrollment type refers to
    # teachers and tas.
    #
    # NOTE: the common_courses and common_groups of the returned
    # MessageableUser objects will be populated only with the context given.
    # this is by design, and desired behavior for the use case this method is
    # directed at. it may be confusing, however, to those not expecting it.
    def messageable_users_in_context(asset_string)
      return [] unless asset_string.sub(/_all\z/, '') =~ CONTEXT_RECIPIENT

      context_type = $1
      context_id = $2.to_i
      enrollment_type = $4
      if enrollment_type == 'admins'
        enrollment_types = ['TeacherEnrollment','TaEnrollment']
      elsif enrollment_type
        enrollment_types = ["#{enrollment_type.capitalize.singularize}Enrollment"]
      end

      case context_type
      when 'course' then messageable_users_in_course(context_id, enrollment_types)
      when 'section' then messageable_users_in_section(context_id, enrollment_types)
      when 'group' then messageable_users_in_group(context_id)
      else []
      end
    end

    # ==========================  end of public API  ==========================
    # |                                                                       |
    # |  the rest of these methods are to simplify the implementation of the  |
    # |  above, and increase testability. they're not private so that they    |
    # |  can be tested in isolation, but should not be used directly          |
    # |                                                                       |
    # =========================================================================

    # given a list of MessageableUsers, loads the common courses between each
    # of those users and the viewing user into the respective MessageableUser
    # objects.
    #
    # the optional include_course_id lets you treat a specific course as if the
    # viewing user had full visibility in it. (TODO: is this right when
    # include_course_id came from a CourseSection admin_context?)
    #
    # the optional strict_checks parameter (default: true) is passed down to
    # the queried scopes (see enrollment_scope and account_user_scope).
    def load_common_courses_with_users(users, include_course_id=nil, strict_checks=true)
      # with messageability constraints, do I see any of the users in any of my visible
      # courses, and if so with which enrollment type(s)?
      Shard.with_each_shard(@user.associated_shards) do
        if all_courses.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          visible_enrollment_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
          end
        end
      end

      # skipping messageability constraints, do I see the user in that specific
      # course, and if so with which enrollment type(s)?
      if include_course_id && course = Course.find_by_id(include_course_id)
        missing_users = users.reject{ |user| user.global_common_courses.keys.include?(course.global_id) }
        if missing_user.present?
          course.shard.activate do
            reverse_lookup = missing_users.index_by(&:id)
            missing_user_ids = reverse_lookup.keys
            enrollment_scope(:strict_checks => strict_checks).where(:id => missing_user_ids, 'courses.id' => course.id).each do |user|
              reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
            end
          end
        end
      end

      # do I see the user in any of the accounts I admin, and if so with what
      # primary enrollment type?
      Shard.with_each_shard(@user.associated_shards) do
        if visible_account_ids.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          visible_account_user_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_courses.merge!(user.global_common_courses)
          end
        end
      end
    end

    # given a list of MessageableUsers, loads the common groups between each
    # of those users and the viewing user into the respective MessageableUser
    # objects.
    #
    # the optional include_group_id lets you treat a specific group as if the
    # viewing user had full visibility in it.
    #
    # the optional strict_checks parameter (default: true) is passed down to
    # the queried scopes (see group_user_scope).
    def load_common_groups_with_users(users, include_group_id=nil, strict_checks=true)
      # with messageability constraints, do I see the user in any of my visible
      # groups?
      Shard.with_each_shard(@user.associated_shards) do
        if fully_visible_group_ids.present?
          reverse_lookup = users.index_by(&:id)
          user_ids = reverse_lookup.keys
          fully_visible_group_user_scope(:strict_checks => strict_checks).where(:id => user_ids).each do |user|
            reverse_lookup[user.id].global_common_groups.merge!(user.global_common_groups)
          end
        end
      end

      # skipping messageability constraints, do I see the user in that specific
      # group?
      if include_group_id && group = Group.find_by_id(include_group_id)
        missing_users = users.reject{ |user| user.global_common_groups.keys.include?(group.global_id) }
        if missing_users.present?
          group.shard.activate do
            reverse_lookup = missing_users.index_by(&:id)
            missing_user_ids = reverse_lookup.keys
            group_user_scope(:strict_checks => strict_checks).where(:id => missing_user_ids, 'group_memberships.group_id' => group.id).each do |user|
              reverse_lookup[user.id].global_common_groups.merge!(user.global_common_groups)
            end
          end
        end
      end
    end

    # filters the provided list of users to just those that are participants in
    # the conversation
    def participants_in_conversation(users, conversation_id)
      conversation = Conversation.find_by_id(conversation_id)
      return [] unless conversation

      conversation.shard.activate do
        reverse_lookup = users.index_by(&:id)
        user_ids = reverse_lookup.keys
        ConversationParticipant.where(
          :user_id => user_ids,
          :conversation_id => conversation_id
        ).all(:select => 'user_id').map{ |cp| reverse_lookup[cp.user_id] }
      end
    end

    # find and return all the messageable users in a particular course (see
    # messageable_users_in_context). the optional enrollment_type restricts to
    # just users with those enrollments
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the course given.
    def messageable_users_in_course(course_id, enrollment_types=nil)
      course = Course.find_by_id(course_id)
      return [] unless course

      course.shard.activate do
        # make sure the course is recognized
        return [] unless course = course_index[course.id]

        scope =
          case course_visibility(course)
          when :full then enrollment_scope(:include_completed => false).where(full_visibility_clause([course]))
          when :sections then enrollment_scope(:include_completed => false).where(section_visibility_clause([course]))
          when :restricted then enrollment_scope(:include_completed => false).where(restricted_visibility_clause([course]))
          end
        scope = scope.where(observer_restriction_clause) if student_courses.present?
        scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
        scope.all
      end
    end

    # find and return all the messageable users in a particular course section
    # (see messageable_users_in_context). the optional enrollment_type
    # restricts to just users with those enrollments
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the course of the given section.
    def messageable_users_in_section(section_id, enrollment_types=nil)
      section = CourseSection.find_by_id(section_id)
      return [] unless section

      section.shard.activate do
        return [] unless course = course_index[section.course_id]
        return [] unless
          fully_visible_courses.include?(course) ||
          section_visible_courses.include?(course) &&
          visible_section_ids_in_courses([course]).include?(section_id)

        scope = enrollment_scope(:include_completed => false).where('enrollments.course_section_id' => section_id)
        scope = scope.where(observer_restriction_clause) if student_courses.present?
        scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
        scope.all
      end
    end

    # find and return all the messageable users in a particular group (see
    # messageable_users_in_context).
    #
    # NOTE: the common_courses of the returned MessageableUser objects will be
    # populated only with the group given.
    def messageable_users_in_group(group_id)
      group = Group.find_by_id(group_id)
      return [] unless group

      group.shard.activate do
        if fully_visible_group_ids.include?(group.id)
          group_user_scope.where('group_memberships.group_id' => group.id).all
        elsif section_visible_group_ids.include?(group.id)
          # group.context is guaranteed to be a course from
          # section_visible_courses at this point
          course = course_index[group.context_id]
          scope = enrollment_scope(:common_group_column => group.id, :include_completed => false).where([
            "course_section_id IN (?) AND EXISTS(SELECT 1 FROM group_memberships WHERE user_id=users.id AND group_id=?)",
            visible_section_ids_in_courses([course]), group.id])
          scope = scope.where(observer_restriction_clause) if student_courses.present?
          scope.all
        else
          return []
{'fraction_non_alphanumeric': 0.07850736707238949, 'fraction_numerical': 0.0011531069827033953, 'mean_word_length': 3.0399844720496896, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 57, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6940543', 'n_tokens_mistral': 8619, 'n_tokens_neox': 8323, 'n_words': 2651}