canvas-lms/app/models/folder.rb

376 lines
13 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 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 '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
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
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
validate_on_update :reject_recursive_folder_structures
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
self.save
end
named_scope :active, :conditions => ['folders.workflow_state != ?', 'deleted']
named_scope :not_hidden, :conditions => ['folders.workflow_state != ?', 'hidden']
named_scope :by_position, :order => 'position'
named_scope :by_name, :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
# You can't lock or hide root folders
if !self.parent_folder_id && (self.locked? || self.hidden? || self.protected?)
self.workflow_state = 'visible'
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
def visible?
# everything but private folders should be visible... for now...
(self.workflow_state == "visible") && (!self.parent_folder || self.parent_folder.visible?)
end
memoize :visible?
def hidden?
self.workflow_state == 'hidden' || (self.parent_folder && self.parent_folder.hidden?)
end
memoize :hidden?
def hidden
hidden?
end
def hidden=(val)
self.workflow_state = (val == true || val == '1' || val == 'true' ? 'hidden' : 'visible')
2011-02-01 09:57:29 +08:00
end
def just_hide
self.workflow_state == 'hidden'
end
def protected?
(self.workflow_state == 'protected') || (self.parent_folder && self.parent_folder.protected?)
end
memoize :protected?
def public?
self.workflow_state == 'public' || (self.parent_folder && self.parent_folder.public?)
end
memoize :public?
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)
root_folders = []
root_folders = context.folders.active.find_all_by_parent_folder_id(nil)
if context.is_a? Course
if root_folders.select{|f| f.name == ROOT_FOLDER_NAME }.empty?
root_folders << context.folders.create(:name => ROOT_FOLDER_NAME, :full_name => ROOT_FOLDER_NAME, :workflow_state => "visible")
2011-02-01 09:57:29 +08:00
end
elsif context.is_a? User
# TODO i18n
t :my_files_folder_name, 'my files'
if root_folders.select{|f| f.name == MY_FILES_FOLDER_NAME }.empty?
root_folders << context.folders.create(:name => MY_FILES_FOLDER_NAME, :full_name => MY_FILES_FOLDER_NAME, :workflow_state => "visible")
2011-02-01 09:57:29 +08:00
end
else
# TODO i18n
t :files_folder_name, 'files'
2011-02-01 09:57:29 +08:00
if root_folders.select{|f| f.name == "files" }.empty?
root_folders << context.folders.create(:name => "files", :full_name => "files", :workflow_state => "visible")
end
end
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.asset_string, path].join('//')
return @@path_lookups[key] if @@path_lookups[key]
folders = path.split('/').select{|f| !f.empty? }
@@root_folders ||= {}
current_folder = (@@root_folders[context.asset_string] ||= Folder.root_folders(context).first)
if folders[0] == current_folder.name
folders.shift
end
folders.each do |name|
sub_folder = @@path_lookups[[context.asset_string, current_folder.full_name + '/' + name].join('//')]
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.asset_string, current_folder.full_name].join('//')] ||= current_folder
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.new(:parent_folder => Folder.root_folders(context).first, :name => 'unfiled')
folder.workflow_state = 'visible'
folder.save
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
2011-02-01 09:57:29 +08:00
def 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?)
end
memoize :locked?
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
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) }#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