canvas-lms/app/models/folder.rb

412 lines
14 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - 2013 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# 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 'set'
2011-02-01 09:57:29 +08:00
class Folder < ActiveRecord::Base
def self.name_order_by_clause(table = nil)
col = table ? "#{table}.name" : 'name'
best_unicode_collation_key(col)
end
2011-02-01 09:57:29 +08:00
include Workflow
attr_accessible :name, :full_name, :parent_folder, :workflow_state, :lock_at, :unlock_at, :locked, :hidden, :context, :position
2011-02-01 09:57:29 +08:00
ROOT_FOLDER_NAME = "course files"
PROFILE_PICS_FOLDER_NAME = "profile pictures"
MY_FILES_FOLDER_NAME = "my files"
save attachments before message creation, fixes #7229 rather than proxy attachments through the conversations controller and cause a long-running db transaction, we now just send them to the right place (files controller, s3, whatever), and then create the message. we now store the attachment in a special folder on the user so that they can more easily be tracked in the future for quota management. because we now just store one instance of each attachment, sending a bulk private message w/ attachments should be a bit less painful. known regression: if a user deletes a conversation attachment from their files area, it deletes if for all recipients. this is essentially the same problem as tickets #6732 and #7481 where we don't let a "deleted" attachment to still be viewed via associations with other objects. test plan: 1. send an attachment on a new conversation and confirm that was sent correctly and can be viewed by recipients 2. send an attachment on an existing conversation and confirm that was sent correctly and can be viewed by recipients 3. send an attachment on a bulk private conversation and 1. confirm that was sent correctly and can be viewed by recipients 2. confirm that only one attachment was actually created, but each message in each conversation is linked to it 4. send multiple attachments and confirm that they were sent correctly and can be viewed by recipients 5. perform steps 1-4 for both local and s3 uploads Change-Id: I7cb21c635f98e47163ef81f0c4050346c64faa91 Reviewed-on: https://gerrit.instructure.com/9046 Reviewed-by: Jon Jensen <jon@instructure.com> Tested-by: Hudson <hudson@instructure.com>
2012-02-28 07:54:00 +08:00
CONVERSATION_ATTACHMENTS_FOLDER_NAME = "conversation attachments"
2011-02-01 09:57:29 +08:00
belongs_to :context, :polymorphic => true
validates_inclusion_of :context_type, :allow_nil => true, :in => ['User', 'Group', 'Account', 'Course']
2011-02-01 09:57:29 +08:00
belongs_to :cloned_item
belongs_to :parent_folder, :class_name => "Folder"
has_many :file_attachments, :class_name => "Attachment"
has_many :active_file_attachments, :class_name => 'Attachment', :conditions => ['attachments.file_state != ?', 'deleted']
has_many :visible_file_attachments, :class_name => 'Attachment', :conditions => ['attachments.file_state in (?, ?)', 'available', 'public']
has_many :sub_folders, :class_name => "Folder", :foreign_key => "parent_folder_id", :dependent => :destroy
has_many :active_sub_folders, :class_name => "Folder", :conditions => ['folders.workflow_state != ?', 'deleted'], :foreign_key => "parent_folder_id", :dependent => :destroy
EXPORTABLE_ATTRIBUTES = [
:id, :name, :full_name, :context_id, :context_type, :parent_folder_id, :workflow_state, :created_at, :updated_at, :deleted_at, :locked,
:lock_at, :unlock_at, :last_lock_at, :last_unlock_at, :cloned_item_id, :position
]
EXPORTABLE_ASSOCIATIONS = [:context, :cloned_item, :parent_folder, :file_attachments, :sub_folders]
2011-02-01 09:57:29 +08:00
acts_as_list :scope => :parent_folder
before_save :infer_full_name
before_save :default_values
after_save :update_sub_folders
after_destroy :clean_up_children
after_save :touch_context
before_save :infer_hidden_state
validates_presence_of :context_id, :context_type
validates_length_of :name, :maximum => maximum_string_length
validate :reject_recursive_folder_structures, on: :update
def reject_recursive_folder_structures
return true if !self.parent_folder_id_changed?
seen_folders = Set.new([self])
folder = self
while folder.parent_folder
folder = folder.parent_folder
if seen_folders.include?(folder)
errors.add(:parent_folder_id, t("errors.invalid_recursion", "A folder cannot be the parent of itself"))
return false
end
seen_folders << folder
end
return true
end
2011-02-01 09:57:29 +08:00
workflow do
# Anyone who has read access to the course can view
state :visible
# Anyone who is an enrolled member of the course can view
state :protected
# Only course admins can view
state :private
# Not sure what this was for...
state :hidden
state :deleted
end
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
self.active_file_attachments.each{|a| a.destroy }
self.active_sub_folders.each{|s| s.destroy }
self.deleted_at = Time.now.utc
2011-02-01 09:57:29 +08:00
self.save
end
scope :active, where("folders.workflow_state<>'deleted'")
scope :not_hidden, where("folders.workflow_state<>'hidden'")
scope :not_locked, lambda { where("(folders.locked IS NULL OR folders.locked=?) AND ((folders.lock_at IS NULL) OR
(folders.lock_at>? OR (folders.unlock_at IS NOT NULL AND folders.unlock_at<?)))", false, Time.now.utc, Time.now.utc) }
scope :by_position, order(:position)
scope :by_name, lambda { order(name_order_by_clause('folders')) }
2011-02-01 09:57:29 +08:00
def display_name
name
end
def full_name(reload=false)
return read_attribute(:full_name) if !reload && read_attribute(:full_name)
folder = self
names = [self.name]
while folder.parent_folder_id do
folder = Folder.find(folder.parent_folder_id) #folder.parent_folder
names << folder.name if folder
end
names.reverse.join("/")
end
def default_values
self.last_unlock_at = self.unlock_at if self.unlock_at
self.last_lock_at = self.lock_at if self.lock_at
if self.parent_folder_id.blank? && ![ROOT_FOLDER_NAME, MY_FILES_FOLDER_NAME, 'files'].include?(self.name)
root_folder = Folder.root_folders(context).first
self.parent_folder_id = root_folder.id
2011-02-01 09:57:29 +08:00
end
end
def infer_hidden_state
self.workflow_state ||= self.parent_folder.workflow_state if self.parent_folder && !self.deleted?
end
protected :infer_hidden_state
def infer_full_name
# TODO i18n
t :default_folder_name, 'folder'
2011-02-01 09:57:29 +08:00
self.name ||= "folder"
self.name = self.name.gsub(/\//, "_")
folder = self
@update_sub_folders = false
self.parent_folder_id = nil if !self.parent_folder || self.parent_folder.context != self.context || self.parent_folder_id == self.id
self.context = self.parent_folder.context if self.parent_folder
self.full_name = self.full_name(true)
if self.parent_folder_id_changed? || !self.parent_folder_id || self.full_name_changed? || self.name_changed?
@update_sub_folders = true
end
@folder_id = self.id
end
protected :infer_full_name
def update_sub_folders
return unless @update_sub_folders
self.sub_folders.each{|f|
f.reload
f.full_name = f.full_name(true)
f.save
}
end
def clean_up_children
Attachment.find_all_by_folder_id(@folder_id).each do |a|
a.destroy
end
end
def subcontent(opts={})
res = []
res += self.active_sub_folders
res += self.active_file_attachments unless opts[:exclude_files]
res
end
2011-02-01 09:57:29 +08:00
def visible?
# everything but private folders should be visible... for now...
return @visible if defined?(@visible)
@visible = (self.workflow_state == "visible") && (!self.parent_folder || self.parent_folder.visible?)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def hidden?
return @hidden if defined?(@hidden)
@hidden = self.workflow_state == 'hidden' || (self.parent_folder && self.parent_folder.hidden?)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def hidden
hidden?
end
2011-02-01 09:57:29 +08:00
def hidden=(val)
self.workflow_state = (val == true || val == '1' || val == 'true' ? 'hidden' : 'visible')
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def just_hide
self.workflow_state == 'hidden'
end
2011-02-01 09:57:29 +08:00
def protected?
return @protected if defined?(@protected)
@protected = (self.workflow_state == 'protected') || (self.parent_folder && self.parent_folder.protected?)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def public?
return @public if defined?(@public)
@public = self.workflow_state == 'public' || (self.parent_folder && self.parent_folder.public?)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def mime_class
"folder"
end
# true if there are any active files or folders
def has_contents?
self.active_file_attachments.any? || self.active_sub_folders.any?
end
2011-02-01 09:57:29 +08:00
attr_accessor :clone_updated
def clone_for(context, dup=nil, options={})
if !self.cloned_item && !self.new_record?
self.cloned_item ||= ClonedItem.create(:original_item => self)
self.save!
end
existing = context.folders.active.find_by_id(self.id)
existing ||= context.folders.active.find_by_cloned_item_id(self.cloned_item_id || 0)
return existing if existing && !options[:overwrite] && !options[:force_copy]
dup ||= Folder.new
dup = existing if existing && options[:overwrite]
self.attributes.delete_if{|k,v| [:id, :full_name, :parent_folder_id].include?(k.to_sym) }.each do |key, val|
dup.send("#{key}=", val)
end
dup.context = context
if options[:include_subcontent] != false
dup.save_without_broadcasting!
self.subcontent.each do |item|
if options[:everything] || options[:all_files] || options[item.asset_string.to_sym]
if item.is_a?(Attachment)
file = item.clone_for(context)
file.folder_id = dup.id
file.save_without_broadcasting!
2011-02-01 09:57:29 +08:00
elsif item.is_a?(Folder)
sub = item.clone_for(context, nil, options)
sub.parent_folder_id = dup.id
sub.save!
end
end
end
end
context.log_merge_result(t :folder_created, "Folder \"%{name}\" created", :name => dup.full_name)
2011-02-01 09:57:29 +08:00
dup.updated_at = Time.now
dup.clone_updated = true
dup
end
def root_folder?
!self.parent_folder_id
end
def self.root_folders(context)
if context.is_a? Course
name = ROOT_FOLDER_NAME
2011-02-01 09:57:29 +08:00
elsif context.is_a? User
name = MY_FILES_FOLDER_NAME
2011-02-01 09:57:29 +08:00
else
name = "files"
2011-02-01 09:57:29 +08:00
end
root_folders = []
# something that doesn't have folders?!
return root_folders unless context.respond_to?(:folders)
context.shard.activate do
Folder.unique_constraint_retry do
root_folder = context.folders.active.find_by_parent_folder_id_and_name(nil, name)
root_folder ||= context.folders.create!(:name => name, :full_name => name, :workflow_state => "visible")
root_folders = [root_folder]
end
end
2011-02-01 09:57:29 +08:00
root_folders
end
def attachments
file_attachments
end
# if a block is given, it'll be called with each new folder created by this
# method before the folder is saved
2011-02-01 09:57:29 +08:00
def self.assert_path(path, context)
@@path_lookups ||= {}
key = [context.global_asset_string, path].join('//')
2011-02-01 09:57:29 +08:00
return @@path_lookups[key] if @@path_lookups[key]
folders = path.split('/').select{|f| !f.empty? }
@@root_folders ||= {}
current_folder = (@@root_folders[context.global_asset_string] ||= Folder.root_folders(context).first)
2011-02-01 09:57:29 +08:00
if folders[0] == current_folder.name
folders.shift
end
folders.each do |name|
sub_folder = @@path_lookups[[context.global_asset_string, current_folder.full_name + '/' + name].join('//')]
2011-02-01 09:57:29 +08:00
sub_folder ||= current_folder.sub_folders.active.find_or_initialize_by_name(name)
current_folder = sub_folder
if current_folder.new_record?
current_folder.context = context
yield current_folder if block_given?
2011-02-01 09:57:29 +08:00
current_folder.save!
end
@@path_lookups[[context.global_asset_string, current_folder.full_name].join('//')] ||= current_folder
2011-02-01 09:57:29 +08:00
end
@@path_lookups[key] = current_folder
end
def self.unfiled_folder(context)
folder = context.folders.find_by_parent_folder_id_and_workflow_state_and_name(Folder.root_folders(context).first.id, 'visible', 'unfiled')
unless folder
folder = context.folders.build(:parent_folder => Folder.root_folders(context).first, :name => 'unfiled')
2011-02-01 09:57:29 +08:00
folder.workflow_state = 'visible'
folder.save!
2011-02-01 09:57:29 +08:00
end
folder
end
def self.find_folder(context, folder_id)
if folder_id
current_folder = context.folders.active.find(folder_id)
else
# TODO i18n
2011-02-01 09:57:29 +08:00
if context.is_a? Course
t :course_content_folder_name, 'course content'
2011-02-01 09:57:29 +08:00
current_folder = context.folders.active.find_by_full_name("course content")
elsif @context.is_a? User
current_folder = context.folders.active.find_by_full_name(MY_FILES_FOLDER_NAME)
2011-02-01 09:57:29 +08:00
end
end
end
def self.find_attachment_in_context_with_path(context, path)
components = path.split('/')
component = components.shift
context.folders.active.find_all_by_parent_folder_id(nil).each do |folder|
if folder.name == component
attachment = folder.find_attachment_with_components(components.dup)
return attachment if attachment
end
end
nil
end
def find_attachment_with_components(components)
component = components.shift
if components.empty?
# find the attachment
return visible_file_attachments.to_a.find {|a| a.matches_filename?(component) }
else
# find a subfolder and recurse (yes, we can have multiple sub-folders w/ the same name)
active_sub_folders.find_all_by_name(component).each do |folder|
a = folder.find_attachment_with_components(components.dup)
return a if a
end
end
nil
end
def get_folders_by_component(components, include_hidden_and_locked)
return [self] if components.empty?
components = components.dup
subfolder_name = components.shift
# search all subfolders with the given name (yes, there can be duplicates)
scope = active_sub_folders.where(name: subfolder_name)
scope = scope.not_hidden.not_locked unless include_hidden_and_locked
scope.each do |subfolder|
sub_components = subfolder.get_folders_by_component(components, include_hidden_and_locked)
return [self] + sub_components if sub_components
end
nil
end
def self.resolve_path(context, path, include_hidden_and_locked = true)
path_components = path ? (path.is_a?(Array) ? path : path.split('/')) : []
Folder.root_folders(context).each do |root_folder|
folders = root_folder.get_folders_by_component(path_components, include_hidden_and_locked)
return folders if folders
end
nil
end
2011-02-01 09:57:29 +08:00
def locked?
return @locked if defined?(@locked)
@locked = self.locked ||
(self.lock_at && Time.now > self.lock_at) ||
(self.unlock_at && Time.now < self.unlock_at) ||
(self.parent_folder && self.parent_folder.locked?)
2011-02-01 09:57:29 +08:00
end
def currently_locked
self.locked || (self.lock_at && Time.now > self.lock_at) || (self.unlock_at && Time.now < self.unlock_at) || self.workflow_state == 'hidden'
end
2011-02-01 09:57:29 +08:00
set_policy do
given { |user, session| self.visible? && self.cached_context_grants_right?(user, session, :read) }#students.include?(user) }
can :read
2011-02-01 09:57:29 +08:00
given { |user, session| self.visible? && !self.locked? && self.cached_context_grants_right?(user, session, :read) && !(self.context.is_a?(Course) && self.context.tab_hidden?(Course::TAB_FILES)) }#students.include?(user) }
can :read_contents
2011-02-01 09:57:29 +08:00
given { |user, session| self.cached_context_grants_right?(user, session, :manage_files) }#admins.include?(user) }
can :update and can :delete and can :create and can :read and can :read_contents
2011-02-01 09:57:29 +08:00
given {|user, session| self.protected? && !self.locked? && self.cached_context_grants_right?(user, session, :read) && self.context.users.include?(user) }
can :read and can :read_contents
2011-02-01 09:57:29 +08:00
end
end