canvas-lms/lib/outcomes/learning_outcome_group_chil...

264 lines
10 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
#
# Copyright (C) 2021 - 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/>.
#
module Outcomes
class LearningOutcomeGroupChildren
include OutcomesFeaturesHelper
include OutcomesServiceAlignmentsHelper
attr_reader :context
SHORT_DESCRIPTION = "coalesce(learning_outcomes.short_description, '')"
# E'<[^>]+>' -> removes html tags
# E'&\\w+;' -> removes html entities
DESCRIPTION = "regexp_replace(regexp_replace(coalesce(learning_outcomes.description, ''), E'<[^>]+>', '', 'gi'), E'&\\w+;', ' ', 'gi')"
MAP_CANVAS_POSTGRES_LOCALES = {
"ar" => "arabic", # العربية
"ca" => "spanish", # Català
"da" => "danish", # Dansk
"da-x-k12" => "danish", # Dansk GR/GY
"de" => "german", # Deutsch
"en-AU" => "english", # English (Australia)
"en-CA" => "english", # English (Canada)
"en-GB" => "english", # English (United Kingdom)
"en" => "english", # English (US)
"es" => "spanish", # Español
"fr" => "french", # Français
"fr-CA" => "french", # Français (Canada)
"it" => "italian", # Italiano
"hu" => "hungarian", # Magyar
"nl" => "dutch", # Nederlands
"nb" => "norwegian", # Norsk (Bokmål)
"nb-x-k12" => "norwegian", # Norsk (Bokmål) GS/VGS
"pt" => "portuguese", # Português
"pt-BR" => "portuguese", # Português do Brasil
"ru" => "russian", # pу́сский
"fi" => "finnish", # Suomi
"sv" => "swedish", # Svenska
"sv-x-k12" => "swedish", # Svenska GR/GY
"tr" => "turkish" # Türkçe
}.freeze
def initialize(context = nil)
@context = context
end
def total_outcomes(learning_outcome_group_id, args = {})
if args == {} && improved_outcomes_management?
cache_key = total_outcomes_cache_key(learning_outcome_group_id)
Rails.cache.fetch(cache_key) do
total_outcomes_for(learning_outcome_group_id, args)
end
else
total_outcomes_for(learning_outcome_group_id, args)
end
end
def not_imported_outcomes(learning_outcome_group_id, args = {})
if group_exists?(args[:target_group_id])
target_group = LearningOutcomeGroup.find_by(id: args[:target_group_id])
source_group_outcome_ids = outcome_links(learning_outcome_group_id).distinct.pluck(:content_id)
target_group_outcome_ids = outcome_links(target_group.id).distinct.pluck(:content_id)
(source_group_outcome_ids - (source_group_outcome_ids & target_group_outcome_ids)).size
end
end
def suboutcomes_by_group_id(learning_outcome_group_id, args = {})
relation = outcome_links(learning_outcome_group_id)
relation = filter_outcomes(relation, args[:filter])
relation = relation.joins(:learning_outcome_content)
.joins("INNER JOIN #{LearningOutcomeGroup.quoted_table_name} AS logs
ON logs.id = content_tags.associated_asset_id")
if args[:search_query]
relation = add_search_query(relation, args[:search_query])
add_search_order(relation, args[:search_query])
else
relation.order(
LearningOutcomeGroup.best_unicode_collation_key("logs.title"),
LearningOutcome.best_unicode_collation_key("short_description")
)
end
end
def clear_total_outcomes_cache
Rails.cache.delete(context_timestamp_cache_key) if improved_outcomes_management?
end
def self.supported_languages
# cache this in the class since this won't change so much
@supported_languages ||= ContentTag.connection.execute(
"SELECT cfgname FROM pg_ts_config"
).to_a.pluck("cfgname")
end
private
def outcome_links(learning_outcome_group_id)
group_ids = children_ids_with_self(learning_outcome_group_id)
relation = ContentTag.active.learning_outcome_links.where(associated_asset_id: group_ids)
# Exclude tags for which the aligned outcome is deleted
valid_outcome_ids = relation
.select("content_tags.content_id")
.joins("LEFT OUTER JOIN #{LearningOutcome.quoted_table_name} AS outcomes ON content_tags.content_id = outcomes.id")
.where("outcomes.workflow_state<>'deleted'")
relation.where(content_id: valid_outcome_ids)
end
def filter_outcomes(relation, filter)
if %w[WITH_ALIGNMENTS NO_ALIGNMENTS].include?(filter) && improved_outcomes_management_enabled?(@context)
outcomes_with_alignments_in_context = ContentTag
.not_deleted
.where(
tag_type: "learning_outcome",
content_type: %w[Rubric Assignment AssessmentQuestionBank],
context: @context
)
.map(&:learning_outcome_id)
.uniq
if outcome_alignment_summary_with_new_quizzes_enabled?(@context)
outcomes_with_alignments_in_os = get_active_os_alignments(@context)
if outcomes_with_alignments_in_os
outcomes_with_alignments_in_context
.concat(outcomes_with_alignments_in_os.keys.map(&:to_i))
.uniq
end
end
return relation.where(content_id: outcomes_with_alignments_in_context) if filter == "WITH_ALIGNMENTS"
return relation.where.not(content_id: outcomes_with_alignments_in_context) if filter == "NO_ALIGNMENTS"
end
relation
end
def total_outcomes_for(learning_outcome_group_id, args = {})
relation = outcome_links(learning_outcome_group_id)
relation = filter_outcomes(relation, args[:filter])
if args[:search_query]
relation = relation.joins(:learning_outcome_content)
relation = add_search_query(relation, args[:search_query])
end
relation.count
end
def add_search_query(relation, search_query)
# Tried to check if the lang is supported in the same query
# using a CASE WHEN but it wont work because it'll
# parse to_tsvector with the not supported lang, and it'll throw an error
sql = if self.class.supported_languages.include?(lang)
ContentTag.sanitize_sql_array([<<~SQL.squish, lang, search_query])
SELECT unnest(tsvector_to_array(to_tsvector(?, ?))) as token
SQL
else
ContentTag.sanitize_sql_array([<<~SQL.squish, search_query])
SELECT unnest(tsvector_to_array(to_tsvector(?))) as token
SQL
end
search_query_tokens = ContentTag.connection.execute(sql).to_a.pluck("token").uniq
short_description_query = ContentTag.sanitize_sql_array(["#{SHORT_DESCRIPTION} ~* ANY(array[?])",
search_query_tokens])
description_query = ContentTag.sanitize_sql_array(["#{DESCRIPTION} ~* ANY(array[?])", search_query_tokens])
relation.where("#{short_description_query} OR #{description_query}")
end
def add_search_order(relation, search_query)
select_query = ContentTag.sanitize_sql_array([<<~SQL.squish, search_query, search_query])
"content_tags".*,
GREATEST(public.word_similarity(?, #{SHORT_DESCRIPTION}), public.word_similarity(?, #{DESCRIPTION})) as sim
SQL
relation.select(select_query).order(
"sim DESC",
LearningOutcomeGroup.best_unicode_collation_key("logs.title"),
LearningOutcome.best_unicode_collation_key("short_description")
)
end
def children_ids_with_self(learning_outcome_group_id)
sql = <<~SQL.squish
WITH RECURSIVE levels AS (
SELECT id, id AS parent_id
FROM (#{LearningOutcomeGroup.active.where(id: learning_outcome_group_id).to_sql}) AS data
UNION ALL
SELECT child.id AS id, parent.parent_id AS parent_id
FROM #{LearningOutcomeGroup.quoted_table_name} child
INNER JOIN levels parent ON parent.id = child.learning_outcome_group_id
WHERE child.workflow_state <> 'deleted'
)
SELECT id FROM levels
SQL
LearningOutcomeGroup.connection.execute(sql).as_json.pluck("id")
end
def context_timestamp_cache
Rails.cache.fetch(context_timestamp_cache_key) do
(Time.zone.now.to_f * 1000).to_i
end
end
def total_outcomes_cache_key(learning_outcome_group_id = nil)
["learning_outcome_group_total_outcomes",
context_asset_string,
context_timestamp_cache,
learning_outcome_group_id].cache_key
end
def context_timestamp_cache_key
["learning_outcome_group_context_timestamp", context_asset_string].cache_key
end
def context_asset_string
@context_asset_string ||= (context || LearningOutcomeGroup.global_root_outcome_group).global_asset_string
end
def improved_outcomes_management?
@improved_outcomes_management ||= if context
context.root_account.feature_enabled?(:improved_outcomes_management)
else
LoadAccount.default_domain_root_account.feature_enabled?(:improved_outcomes_management)
end
end
def group_exists?(learning_outcome_group_id)
LearningOutcomeGroup.find_by(id: learning_outcome_group_id) != nil
end
def lang
# lang can be nil, so we check with instance_variable_defined? method
unless instance_variable_defined?(:@lang)
account = context&.root_account || LoadAccount.default_domain_root_account
@lang = MAP_CANVAS_POSTGRES_LOCALES[account.default_locale || "en"]
end
@lang
end
end
end