Website : rimsha.abasa.com
backdoor
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
var
/
canvas
/
app
/
models
/
Filename :
context_module.rb
back
Copy
# frozen_string_literal: true # # Copyright (C) 2011 - 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 ContextModule < ActiveRecord::Base include Workflow include SearchTermHelper include DuplicatingObjects include LockedFor include DifferentiableAssignment include MasterCourses::Restrictor restrict_columns :state, [:workflow_state] restrict_columns :settings, %i[prerequisites completion_requirements requirement_count require_sequential_progress] belongs_to :context, polymorphic: [:course] belongs_to :root_account, class_name: "Account" has_many :context_module_progressions, dependent: :destroy has_many :content_tags, -> { order("content_tags.position, content_tags.title") }, dependent: :destroy has_many :assignment_overrides, dependent: :destroy, inverse_of: :context_module has_many :assignment_override_students, dependent: :destroy has_one :master_content_tag, class_name: "MasterCourses::MasterContentTag", inverse_of: :context_module acts_as_list scope: { context: self, workflow_state: ["active", "unpublished"] } serialize :prerequisites serialize :completion_requirements before_save :infer_position before_save :validate_prerequisites before_save :confirm_valid_requirements before_save :set_root_account_id after_save :touch_context after_save :invalidate_progressions after_save :relock_warning_check after_save :clear_discussion_stream_items after_save :send_items_to_stream validates :workflow_state, :context_id, :context_type, presence: true validates :name, presence: { if: :require_presence_of_name } attr_accessor :require_presence_of_name def relock_warning_check # if the course is already active and we're adding more stringent requirements # then we're going to give the user an option to re-lock students out of the modules # otherwise they will be able to continue as before @relock_warning = false return if new_record? if context.available? && active? if saved_change_to_workflow_state? && workflow_state_before_last_save == "unpublished" # should trigger when publishing a prerequisite for an already active module @relock_warning = true if context.context_modules.active.any? { |mod| is_prerequisite_for?(mod) } # if any of these changed while we were unpublished, then we also need to trigger @relock_warning = true if prerequisites.any? || completion_requirements.any? || unlock_at.present? end if saved_change_to_completion_requirements? && (completion_requirements.to_a - completion_requirements_before_last_save.to_a).present? # removing a requirement shouldn't trigger @relock_warning = true end if saved_change_to_prerequisites? && (prerequisites.to_a - prerequisites_before_last_save.to_a).present? # ditto with removing a prerequisite @relock_warning = true end if saved_change_to_unlock_at? && unlock_at.present? && unlock_at_before_last_save.blank? # adding a unlock_at date should trigger @relock_warning = true end end end def relock_warning? @relock_warning end def relock_progressions(relocked_modules = [], student_ids = nil) return if relocked_modules.include?(self) self.class.connection.after_transaction_commit do relocked_modules << self progression_scope = context_module_progressions.where.not(workflow_state: "locked") progression_scope = progression_scope.where(user_id: student_ids) if student_ids if progression_scope.in_batches(of: 10_000).update_all(["workflow_state = 'locked', lock_version = lock_version + 1, current = ?", false]) > 0 delay_if_production(n_strand: ["evaluate_module_progressions", global_context_id], singleton: "evaluate_module_progressions:#{global_id}") .evaluate_all_progressions end context.context_modules.each do |mod| mod.relock_progressions(relocked_modules, student_ids) if is_prerequisite_for?(mod) end end end def invalidate_progressions self.class.connection.after_transaction_commit do if context_module_progressions.where(current: true).in_batches(of: 10_000).update_all(current: false) > 0 # don't queue a job unless necessary delay_if_production(n_strand: ["evaluate_module_progressions", global_context_id], singleton: "evaluate_module_progressions:#{global_id}") .evaluate_all_progressions end @discussion_topics_to_recalculate&.each do |dt| dt.delay_if_production(n_strand: ["evaluate_discussion_topic_progressions", global_context_id], singleton: "evaluate_discussion_topic_progressions:#{dt.global_id}") .recalculate_context_module_actions! end end end def evaluate_all_progressions current_column = "context_module_progressions.current" current_scope = context_module_progressions.where("#{current_column} IS NULL OR #{current_column} = ?", false).preload(:user) current_scope.find_in_batches(batch_size: 100) do |progressions| context.cache_item_visibilities_for_user_ids(progressions.map(&:user_id)) progressions.each do |progression| progression.context_module = self progression.evaluate! end context.clear_cached_item_visibilities end end def check_for_stale_cache_after_unlocking! GuardRail.activate(:primary) { touch } if unlock_at && unlock_at < Time.now && updated_at < unlock_at end def is_prerequisite_for?(mod) (mod.prerequisites || []).any? { |prereq| prereq[:type] == "context_module" && prereq[:id] == id } end def self.module_positions(context) # Keep a cached hash of all modules for a given context and their # respective positions -- used when enforcing valid prerequisites # and when generating the list of downstream modules Rails.cache.fetch(["module_positions", context].cache_key) do hash = {} context.context_modules.not_deleted.each { |m| hash[m.id] = m.position || 0 } hash end end def remove_completion_requirement(id) if completion_requirements.present? new_requirements = completion_requirements.delete_if do |requirement| requirement[:id] == id end update_attribute :completion_requirements, new_requirements end end def infer_position unless position positions = ContextModule.module_positions(context) self.position = if (max = positions.values.max) max + 1 else 1 end end position end def get_potentially_conflicting_titles(title_base) ContextModule.not_deleted.where(context_id:) .starting_with_name(title_base).pluck("name").to_set end def duplicate_base_model(copy_title) ContextModule.new({ context_id:, context_type:, name: copy_title, position: ContextModule.not_deleted.where(context_id:).maximum(:position) + 1, completion_requirements:, workflow_state: "unpublished", require_sequential_progress:, completion_events:, requirement_count: }) end def can_be_duplicated? content_tags.none? do |content_tag| !content_tag.deleted? && content_tag.content_type_class == "quiz" end end def send_items_to_stream if saved_change_to_workflow_state? && workflow_state == "active" content_tags.where(content_type: "DiscussionTopic", workflow_state: "active").preload(:content).each do |ct| ct.content.send_items_to_stream end end end def clear_discussion_stream_items if saved_change_to_workflow_state? && ["active", nil].include?(workflow_state_before_last_save) && workflow_state == "unpublished" content_tags.where(content_type: "DiscussionTopic", workflow_state: "active").preload(:content).each do |ct| ct.content.clear_stream_items end end end # This is intended for duplicating a content tag when we are duplicating a module # Not intended for duplicating a content tag to keep in the original module def duplicate_content_tag_base_model(original_content_tag) ContentTag.new( content_id: original_content_tag.content_id, content_type: original_content_tag.content_type, context_id: original_content_tag.context_id, context_type: original_content_tag.context_type, url: original_content_tag.url, new_tab: original_content_tag.new_tab, title: original_content_tag.title, tag_type: original_content_tag.tag_type, position: original_content_tag.position, indent: original_content_tag.indent, learning_outcome_id: original_content_tag.learning_outcome_id, context_code: original_content_tag.context_code, mastery_score: original_content_tag.mastery_score, workflow_state: "unpublished" ) end private :duplicate_content_tag_base_model # Intended for taking a content_tag in this module and duplicating it # into a new module. Not intended for duplicating a content tag to be # kept in the same module. def duplicate_content_tag(original_content_tag) new_tag = duplicate_content_tag_base_model(original_content_tag) if original_content_tag.content.respond_to?(:duplicate) new_tag.content = original_content_tag.content.duplicate # If we have multiple assignments (e.g.) make sure they each get unused titles. # A title isn't marked used if the assignment hasn't been saved yet. new_tag.content.save! new_tag.title = nil end new_tag end private :duplicate_content_tag def set_root_account_id self.root_account_id ||= context&.root_account_id end def only_visible_to_overrides assignment_overrides.active.exists? end def visible_to_everyone !only_visible_to_overrides end def duplicate copy_title = get_copy_title(self, t("Copy"), name) new_module = duplicate_base_model(copy_title) living_tags = content_tags.reject(&:deleted?) new_module.content_tags = living_tags.map do |content_tag| duplicate_content_tag(content_tag) end new_module end def validate_prerequisites positions = ContextModule.module_positions(context) @already_confirmed_valid_requirements = false prereqs = [] (prerequisites || []).each do |pre| if pre[:type] == "context_module" position = positions[pre[:id].to_i] || 0 prereqs << pre if position && position < (self.position || 0) else prereqs << pre end end self.prerequisites = prereqs self.position end alias_method :destroy_permanently!, :destroy def destroy self.workflow_state = "deleted" self.deleted_at = Time.now.utc module_assignments = current_items_with_assignment ContentTag.where(context_module_id: self).where.not(workflow_state: "deleted").update(workflow_state: "deleted", updated_at: deleted_at) delay_if_production(n_strand: "context_module_update_downstreams", priority: Delayed::LOW_PRIORITY).update_downstreams save! update_assignment_submissions(module_assignments) true end def restore if workflow_state == "deleted" && deleted_at # only restore tags deleted (approximately) when the module was deleted # (tags are currently set to exactly deleted_at but older deleted modules used the current time on each tag) tags_to_restore = content_tags.where(workflow_state: "deleted") .where("updated_at BETWEEN ? AND ?", deleted_at - 5.seconds, deleted_at + 5.seconds) .preload(:content) tags_to_restore.each do |tag| # don't restore the item if the asset has been deleted too next if tag.asset_workflow_state == "deleted" # although the module will be restored unpublished, the items should match the asset's published state tag.workflow_state = if tag.content && tag.sync_workflow_state_to_asset? tag.asset_workflow_state else "unpublished" end # deal with the possibility that the asset has been renamed after the module was deleted tag.title = Context.asset_name(tag.content) if tag.content && tag.sync_title_to_asset_title? tag.save end end self.workflow_state = "unpublished" save end def update_downstreams(_original_position = nil) # TODO: remove the unused argument; it's not sent anymore, but it was sent through a delayed job # so compatibility was maintained when sender was updated to not send it positions = ContextModule.module_positions(context).to_a.sort_by { |a| a[1] } downstream_ids = positions.select { |a| a[1] > (position || 0) }.pluck(0) downstreams = downstream_ids.empty? ? [] : context.context_modules.not_deleted.where(id: downstream_ids) downstreams.each(&:save_without_touching_context) end workflow do state :active do event :unpublish, transitions_to: :unpublished end state :unpublished do event :publish, transitions_to: :active end state :deleted end scope :active, -> { where(workflow_state: "active") } scope :unpublished, -> { where(workflow_state: "unpublished") } scope :not_deleted, -> { where("context_modules.workflow_state<>'deleted'") } scope :starting_with_name, lambda { |name| where("name ILIKE ?", "#{name}%") } scope :visible_to_students_in_course_with_da, lambda { |user_ids, course_ids| visible_module_ids = ModuleVisibility::ModuleVisibilityService.modules_visible_to_students_in_courses(course_ids:, user_ids:).map(&:context_module_id) if visible_module_ids.any? where(id: visible_module_ids) else none end } alias_method :published?, :active? def publish_items!(progress: nil) content_tags.each do |content_tag| break if progress&.reload&.failed? content_tag.trigger_publish! end end def unpublish_items!(progress: nil) content_tags.each do |content_tag| break if progress&.reload&.failed? content_tag.trigger_unpublish! end end set_policy do #################### Begin legacy permission block ######################### given do |user, session| user && !context.root_account.feature_enabled?(:granular_permissions_manage_course_content) && context.grants_right?(user, session, :manage_content) end can :read and can :create and can :update and can :delete and can :read_as_admin ##################### End legacy permission block ########################## given do |user, session| user && context.root_account.feature_enabled?(:granular_permissions_manage_course_content) && context.grants_right?(user, session, :manage_course_content_add) end can :read and can :read_as_admin and can :create given do |user, session| user && context.root_account.feature_enabled?(:granular_permissions_manage_course_content) && context.grants_right?(user, session, :manage_course_content_edit) end can :read and can :read_as_admin and can :update given do |user, session| user && context.root_account.feature_enabled?(:granular_permissions_manage_course_content) && context.grants_right?(user, session, :manage_course_content_delete) end can :read and can :read_as_admin and can :delete given { |user, session| context.grants_right?(user, session, :read_as_admin) } can :read and can :read_as_admin given { |user, session| context.grants_right?(user, session, :view_unpublished_items) } can :view_unpublished_items given { |user, session| context.grants_right?(user, session, :read) && active? } can :read given { |user, session| user && context.grants_any_right?(user, session, :manage_content, :manage_course_content_edit) } can :manage_assign_to end def low_level_locked_for?(user, opts = {}) return false if grants_right?(user, :read_as_admin) available = available_for?(user, opts) return { object: self, module: self } unless available return { object: self, module: self, unlock_at: } if to_be_unlocked false end def available_for?(user, opts = {}) return true if active? && !to_be_unlocked && prerequisites.blank? && (completion_requirements.empty? || !require_sequential_progress) if grants_right?(user, :read_as_admin) return true elsif !active? return false elsif context.user_has_been_observer?(user) # rubocop:disable Lint/DuplicateBranch return true end progression = if opts[:user_context_module_progressions] opts[:user_context_module_progressions][id] end progression ||= find_or_create_progression(user) # if the progression is locked, then position in the progression doesn't # matter. we're not available. tag = opts[:tag] avail = progression && !progression.locked? && !locked_for_tag?(tag, progression) if !avail && opts[:deep_check_if_needed] progression = evaluate_for(progression) avail = progression && !progression.locked? && !locked_for_tag?(tag, progression) end avail end def locked_for_tag?(tag, progression) locked = tag&.context_module_id == id && require_sequential_progress locked && (progression.current_position&.< tag.position) end def self.module_names(context) Rails.cache.fetch(["module_names", context].cache_key) do gather_module_names(context.context_modules.not_deleted) end end def self.active_module_names(context) Rails.cache.fetch(["active_module_names", context].cache_key) do gather_module_names(context.context_modules.active) end end def self.gather_module_names(scope) scope.pluck(:id, :name).each_with_object({}) do |(id, name), names| names[id] = name end end def prerequisites @prerequisites ||= gather_prerequisites(ContextModule.module_names(context)) end def active_prerequisites @active_prerequisites ||= gather_prerequisites(ContextModule.active_module_names(context)) end def gather_prerequisites(module_names) all_prereqs = read_attribute(:prerequisites) return [] unless all_prereqs&.any? all_prereqs.select { |pre| module_names.key?(pre[:id]) }.map { |pre| pre.merge(name: module_names[pre[:id]]) } end def prerequisites=(prereqs) Rails.cache.delete(["module_names", context].cache_key) # ensure the module list is up to date case prereqs when Array # validate format, skipping invalid ones prereqs = prereqs.select do |pre| pre.key?(:id) && pre.key?(:name) && pre[:type] == "context_module" end when String res = [] module_names = ContextModule.module_names(context) pres = prereqs.split(",") pre_regex = /module_(\d+)/ pres.each do |pre| next unless (match = pre_regex.match(pre)) id = match[1].to_i if module_names.key?(id) res << { id:, type: "context_module", name: module_names[id] } end end prereqs = res else prereqs = nil end @prerequisites = nil @active_prerequisites = nil write_attribute(:prerequisites, prereqs) end def completion_requirements=(val) if val.is_a?(Array) hash = {} val.each { |i| hash[i[:id]] = i } val = hash end if val.is_a?(Hash) # requirements hash can contain invalid data (e.g. {"none"=>"none"}) from the ui, # filter & manipulate the data to something more reasonable val = val.map do |id, req| if req.is_a?(Hash) req[:id] = id unless req[:id] req end end val = validate_completion_requirements(val.compact) else val = nil end write_attribute(:completion_requirements, val) end def validate_completion_requirements(requirements) requirements = requirements.map do |req| new_req = { id: req[:id].to_i, type: req[:type], } new_req[:min_score] = req[:min_score].to_f if req[:type] == "min_score" && req[:min_score] new_req end tags = content_tags.not_deleted.index_by(&:id) validated_reqs = requirements.select do |req| if req[:id] && (tag = tags[req[:id]]) if %w[must_view must_mark_done must_contribute].include?(req[:type]) true elsif %w[must_submit min_score].include?(req[:type]) true if tag.scoreable? end end end unless new_record? old_requirements = completion_requirements || [] validated_reqs.each do |req| next unless req[:type] == "must_contribute" && !old_requirements.detect { |r| r[:id] == req[:id] && r[:type] == req[:type] } # new requirement tag = tags[req[:id]] if tag.content_type == "DiscussionTopic" @discussion_topics_to_recalculate ||= [] @discussion_topics_to_recalculate << tag.content end end end validated_reqs end def completion_requirements_visible_to(user, opts = {}) valid_ids = content_tags_visible_to(user, opts).map(&:id) completion_requirements.select { |cr| valid_ids.include? cr[:id] } end def content_tags_visible_to(user, opts = {}) @content_tags_visible_to ||= {} @content_tags_visible_to[user.try(:id)] ||= begin is_teacher = opts[:is_teacher] != false && grants_right?(user, :read_as_admin) tags = is_teacher ? cached_not_deleted_tags : cached_active_tags if !is_teacher && user opts[:is_teacher] = false tags = filter_tags_for_da(tags, user, opts) end # always return an array now because filter_tags_for_da *might* return one tags.to_a end end def visibility_for_user(user, session = nil) opts = {} opts[:can_read] = context.grants_right?(user, session, :read) if opts[:can_read] opts[:can_read_as_admin] = context.grants_right?(user, session, :read_as_admin) end opts end def filter_tags_for_da(tags, user, opts = {}) filter = proc do |inner_tags, user_ids| visible_item_ids = {} inner_tags.select do |tag| item_type = case tag.content_type when "Assignment" :assignment when "DiscussionTopic" :discussion when "WikiPage" :page when *Quizzes::Quiz.class_names :quiz end if item_type visible_item_ids[item_type] ||= context.visible_item_ids_for_users(item_type, user_ids) # don't load the visibilities if there are no items of that type visible_item_ids[item_type].include?(tag.content_id) else true end end end shard.activate do DifferentiableAssignment.filter(tags, user, context, opts) do |ts, user_ids| filter.call(ts, user_ids, context_id, opts) end end end def reload @prerequisites = nil @active_prerequisites = nil clear_cached_lookups super end def clear_cached_lookups @cached_active_tags = nil @cached_not_deleted_tags = nil @content_tags_visible_to = nil end def cached_active_tags @cached_active_tags ||= if content_tags.loaded? # don't reload the preloaded content content_tags.select(&:active?) else content_tags.active.to_a end end def cached_not_deleted_tags @cached_not_deleted_tags ||= if content_tags.loaded? # don't reload the preloaded content content_tags.reject(&:deleted?) else content_tags.not_deleted.to_a end end def add_item(params, added_item = nil, opts = {}) params[:type] = params[:type].underscore if params[:type] top_position = (content_tags.not_deleted.maximum(:position) || 0) + 1 position = opts[:position] || top_position position = [position, params[:position].to_i].max if params[:position] if content_tags.not_deleted.where(position:).count != 0 position = top_position end case params[:type] when "wiki_page", "page" item = opts[:wiki_page] || context.wiki_pages.where(id: params[:id]).first when "attachment", "file" item = opts[:attachment] || context.attachments.not_deleted.find_by(id: params[:id]) when "assignment" item = opts[:assignment] || context.assignments.active.where(id: params[:id]).first item = item.submittable_object if item.respond_to?(:submittable_object) && item.submittable_object when "discussion_topic", "discussion" item = opts[:discussion_topic] || context.discussion_topics.active.where(id: params[:id]).first when "quiz" item = opts[:quiz] || context.quizzes.active.where(id: params[:id]).first end workflow_state = ContentTag.asset_workflow_state(item) if item workflow_state ||= "active" case params[:type] when "external_url" title = params[:title] added_item ||= content_tags.build(context:) added_item.attributes = { url: params[:url], new_tab: params[:new_tab], tag_type: "context_module", title:, indent: params[:indent], position: } added_item.content_id = 0 added_item.content_type = "ExternalUrl" added_item.context_module_id = id added_item.indent = params[:indent] || 0 added_item.workflow_state = "unpublished" if added_item.new_record? when "context_external_tool", "external_tool", "lti/message_handler" title = params[:title] added_item ||= content_tags.build(context:) content = if params[:type] == "lti/message_handler" Lti::MessageHandler.for_context(context).where(id: params[:id]).first else ContextExternalTool.find_external_tool(params[:url], context, params[:id].to_i) || ContextExternalTool.new.tap { |tool| tool.id = 0 } end added_item.attributes = { content:, url: params[:url], new_tab: params[:new_tab], tag_type: "context_module", title:, indent: params[:indent], position: } added_item.context_module_id = id added_item.indent = params[:indent] || 0 added_item.workflow_state = "unpublished" if added_item.new_record? added_item.link_settings = params[:link_settings] if content.is_a?(ContextExternalTool) && content.use_1_3? && content.id != 0 # This method is called both to create a module item and to update one # (e.g. in a blueprint course sync.) # # For new module items (or old module items that don't have a resource # link), we create a new ResourceLink if one cannot be found for the # lookup_uuid, or if lookup_uuid is not given. added_item.associated_asset ||= Lti::ResourceLink.find_or_initialize_for_context_and_lookup_uuid( context:, lookup_uuid: params[:lti_resource_link_lookup_uuid].presence, custom: Lti::DeepLinkingUtil.validate_custom_params(params[:custom_params]), context_external_tool: content, url: params[:url] ) end when "context_module_sub_header", "sub_header" title = params[:title] added_item ||= content_tags.build(context:) added_item.attributes = { tag_type: "context_module", title:, indent: params[:indent], position: } added_item.content_id = 0 added_item.content_type = "ContextModuleSubHeader" added_item.context_module_id = id added_item.indent = params[:indent] || 0 added_item.workflow_state = "unpublished" if added_item.new_record? else return nil unless item title = params[:title] || (item.title rescue item.name) added_item ||= content_tags.build(context:) added_item.attributes = { content: item, tag_type: "context_module", title:, indent: params[:indent], position: } added_item.context_module_id = id added_item.indent = params[:indent] || 0 added_item.workflow_state = workflow_state if added_item.new_record? end added_item.save added_item end # specify a 1-based position to insert the items at; leave nil to append to the end of the module # ignores current module item positions in favor of an objective position def insert_items(items, start_pos = nil) tags = content_tags.not_deleted.select(:id, :position, :content_type, :content_id).to_a if start_pos start_pos = 1 if start_pos < 1 next_pos = start_pos else next_pos = (content_tags.maximum(:position) || 0) + 1 end new_tags = [] items.each do |item| next unless item.is_a?(ActiveRecord::Base) next unless %w[Attachment Assignment WikiPage Quizzes::Quiz DiscussionTopic ContextExternalTool].include?(item.class_name) item = item.submittable_object if item.is_a?(Assignment) && item.submittable_object next if tags.any? { |tag| tag.content_type == item.class_name && tag.content_id == item.id } state = (item.respond_to?(:published?) && !item.published?) ? "unpublished" : "active" new_tags << content_tags.create!(context:, title: Context.asset_name(item), content: item, tag_type: "context_module", indent: 0, position: next_pos, workflow_state: state) next_pos += 1 end return unless start_pos tag_ids_to_move = {} tags_before = (start_pos < 2) ? [] : tags[0..start_pos - 2] tags_after = (start_pos > tags.length) ? [] : tags[start_pos - 1..] (tags_before + new_tags + tags_after).each_with_index do |item, index| index_change = index + 1 - item.position if index_change != 0 tag_ids_to_move[index_change] ||= [] tag_ids_to_move[index_change] << item.id end end tag_ids_to_move.each do |position_change, ids| content_tags.where(id: ids).update_all(sanitize_sql(["position = position + ?", position_change])) end end def update_for(user, action, tag, points = nil) return nil unless context.grants_right?(user, :participate_as_student) return nil unless (progression = evaluate_for(user)) return nil if progression.locked? progression.update_requirement_met!(action, tag, points) progression end def completion_requirement_for(action, tag) completion_requirements.to_a.find do |requirement| next false unless requirement[:id] == tag.local_id case requirement[:type] when "must_view" action == :read || action == :contributed when "must_mark_done" action == :done when "must_contribute" action == :contributed when "must_submit", "min_score" action == :scored || # rubocop:disable Style/MultipleComparison action == :submitted # to mark progress in the incomplete_requirements (moves from 'unlocked' to 'started') else false end end end def self.requirement_description(req) case req[:type] when "must_view" t("requirements.must_view", "must view the page") when "must_mark_done" t("must mark as done") when "must_contribute" t("requirements.must_contribute", "must contribute to the page") when "must_submit" t("requirements.must_submit", "must submit the assignment") when "min_score" t("requirements.min_score", "must score at least a %{score}", score: req[:min_score]) else nil end end def confirm_valid_requirements(do_save = false) return if @already_confirmed_valid_requirements @already_confirmed_valid_requirements = true # the write accessor validates for us self.completion_requirements = completion_requirements || [] save if do_save && completion_requirements_changed? completion_requirements end def find_or_create_progressions(users) users = Array(users) users_hash = {} users.each { |u| users_hash[u.id] = u } progressions = context_module_progressions.where(user_id: users) progressions_hash = {} progressions.each { |p| progressions_hash[p.user_id] = p } newbies = users.reject { |u| progressions_hash[u.id] } progressions += newbies.map { |u| find_or_create_progression(u) } progressions.each { |p| p.user = users_hash[p.user_id] } progressions.uniq end def find_or_create_progression(user) return nil unless user shard.activate do GuardRail.activate(:primary) do if context.enrollments.except(:preload).where(user_id: user).exists? ContextModuleProgression.create_and_ignore_on_duplicate(user:, context_module: self) end end end end def evaluate_for(user_or_progression) if user_or_progression.is_a?(ContextModuleProgression) progression, user = [user_or_progression, user_or_progression.user] elsif user_or_progression progression, user = [find_or_create_progression(user_or_progression), user_or_progression] end return nil unless progression && user progression.context_module = self if progression.context_module_id == id progression.user = user if progression.user_id == user.id progression.evaluate! end def to_be_unlocked unlock_at && unlock_at > Time.now end def migration_position @migration_position_counter ||= 0 @migration_position_counter += 1 end attr_accessor :item_migration_position VALID_COMPLETION_EVENTS = [:publish_final_grade].freeze def completion_events (read_attribute(:completion_events) || "").split(",").map(&:to_sym) end def completion_events=(value) unless value.present? write_attribute(:completion_events, nil) return end write_attribute(:completion_events, (value.map(&:to_sym) & VALID_COMPLETION_EVENTS).join(",")) end VALID_COMPLETION_EVENTS.each do |event| class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{event}=(value) if Canvas::Plugin.value_to_boolean(value) self.completion_events |= [:#{event}] else self.completion_events -= [:#{event}] end end def #{event}? completion_events.include?(:#{event}) end RUBY end def completion_event_callbacks callbacks = [] if publish_final_grade? && (plugin = Canvas::Plugin.find("grade_export")) && plugin.enabled? callbacks << ->(user) { context.publish_final_grades(user, user.id) } end callbacks end def requirement_type (completion_requirements.present? && requirement_count == 1) ? "one" : "all" end def all_assignment_overrides assignment_overrides end def update_assignment_submissions(module_assignments = current_items_with_assignment) if Account.site_admin.feature_enabled?(:selective_release_backend) module_assignments.clear_cache_keys(:availability) SubmissionLifecycleManager.recompute_course(context, assignments: module_assignments, update_grades: true) end end def current_items_with_assignment return unless Account.site_admin.feature_enabled?(:selective_release_backend) module_assignments = Assignment.active.where(id: content_tags.not_deleted.where(content_type: "Assignment").select(:content_id)).pluck(:id) module_discussions_assignment_ids = DiscussionTopic.active.where(id: content_tags.not_deleted.where(content_type: "DiscussionTopic").select(:content_id)).select(:assignment_id) module_quizzes_assignment_ids = Quizzes::Quiz.active.where(id: content_tags.not_deleted.where(content_type: "Quizzes::Quiz").select(:content_id)).select(:assignment_id) module_quizzes_and_discussions = Assignment.active.where(id: module_discussions_assignment_ids).select(:id) module_quizzes_and_discussions += Assignment.active.where(id: module_quizzes_assignment_ids).select(:id) assignments_quizzes = module_assignments + module_quizzes_and_discussions Assignment.where(id: assignments_quizzes) end end