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/>.
#
class Group < ActiveRecord :: Base
include Context
include Workflow
2011-09-22 04:31:36 +08:00
attr_accessible :name , :context , :max_membership , :group_category , :join_level , :default_view
2011-02-09 01:15:49 +08:00
has_many :group_memberships , :dependent = > :destroy , :conditions = > [ 'group_memberships.workflow_state != ?' , 'deleted' ]
has_many :users , :through = > :group_memberships , :conditions = > [ 'users.workflow_state != ?' , 'deleted' ]
2011-02-01 09:57:29 +08:00
has_many :participating_group_memberships , :class_name = > " GroupMembership " , :conditions = > [ 'group_memberships.workflow_state = ?' , 'accepted' ]
has_many :participating_users , :source = > :user , :through = > :participating_group_memberships
has_many :invited_group_memberships , :class_name = > " GroupMembership " , :conditions = > [ 'group_memberships.workflow_state = ?' , 'invited' ]
has_many :invited_users , :source = > :user , :through = > :invited_group_memberships
belongs_to :context , :polymorphic = > true
2011-09-22 04:31:36 +08:00
belongs_to :group_category
2011-02-01 09:57:29 +08:00
belongs_to :account
2011-06-03 23:54:00 +08:00
belongs_to :root_account , :class_name = > " Account "
2011-02-01 09:57:29 +08:00
has_many :calendar_events , :as = > :context , :dependent = > :destroy
has_many :discussion_topics , :as = > :context , :conditions = > [ 'discussion_topics.workflow_state != ?' , 'deleted' ] , :include = > :user , :dependent = > :destroy , :order = > 'discussion_topics.position DESC, discussion_topics.created_at DESC'
has_many :active_discussion_topics , :as = > :context , :class_name = > 'DiscussionTopic' , :conditions = > [ 'discussion_topics.workflow_state != ?' , 'deleted' ] , :include = > :user
has_many :all_discussion_topics , :as = > :context , :class_name = > " DiscussionTopic " , :include = > :user , :dependent = > :destroy
has_many :discussion_entries , :through = > :discussion_topics , :include = > [ :discussion_topic , :user ] , :dependent = > :destroy
has_many :announcements , :as = > :context , :class_name = > 'Announcement' , :dependent = > :destroy
has_many :active_announcements , :as = > :context , :class_name = > 'Announcement' , :conditions = > [ 'discussion_topics.workflow_state != ?' , 'deleted' ]
2011-09-29 07:26:18 +08:00
has_many :attachments , :as = > :context , :dependent = > :destroy , :extend = > Attachment :: FindInContextAssociation
2011-07-12 02:25:54 +08:00
has_many :active_images , :as = > :context , :class_name = > 'Attachment' , :conditions = > [ " attachments.file_state != ? AND attachments.content_type LIKE 'image%' " , 'deleted' ] , :order = > 'attachments.display_name' , :include = > :thumbnail
2011-02-01 09:57:29 +08:00
has_many :active_assignments , :as = > :context , :class_name = > 'Assignment' , :conditions = > [ 'assignments.workflow_state != ?' , 'deleted' ]
has_many :all_attachments , :as = > 'context' , :class_name = > 'Attachment'
has_many :folders , :as = > :context , :dependent = > :destroy , :order = > 'folders.name'
has_many :active_folders , :class_name = > 'Folder' , :as = > :context , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
2011-03-02 03:17:42 +08:00
has_many :active_folders_with_sub_folders , :class_name = > 'Folder' , :as = > :context , :include = > [ :active_sub_folders ] , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
2011-02-01 09:57:29 +08:00
has_many :active_folders_detailed , :class_name = > 'Folder' , :as = > :context , :include = > [ :active_sub_folders , :active_file_attachments ] , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
has_many :external_feeds , :as = > :context , :dependent = > :destroy
has_many :messages , :as = > :context , :dependent = > :destroy
belongs_to :wiki
has_many :default_wiki_wiki_pages , :class_name = > 'WikiPage' , :through = > :wiki , :source = > :wiki_pages
has_many :active_default_wiki_wiki_pages , :class_name = > 'WikiPage' , :through = > :wiki , :source = > :wiki_pages , :conditions = > [ 'wiki_pages.workflow_state = ?' , 'active' ]
has_many :wiki_namespaces , :as = > :context , :dependent = > :destroy
has_many :web_conferences , :as = > :context , :dependent = > :destroy
has_many :tags , :class_name = > 'ContentTag' , :as = > 'context' , :order = > 'LOWER(title)' , :dependent = > :destroy
has_many :collaborations , :as = > :context , :order = > 'title, created_at' , :dependent = > :destroy
has_one :scribd_account , :as = > :scribdable
has_many :short_message_associations , :as = > :context , :include = > :short_message , :dependent = > :destroy
has_many :short_messages , :through = > :short_message_associations , :dependent = > :destroy
has_many :media_objects , :as = > :context
2011-12-10 06:37:12 +08:00
has_many :zip_file_imports , :as = > :context
2012-01-04 04:30:49 +08:00
2011-09-22 04:31:36 +08:00
before_save :ensure_defaults , :maintain_category_attribute
2011-02-01 09:57:29 +08:00
after_save :close_memberships_if_deleted
2012-01-04 04:30:49 +08:00
2011-09-22 01:36:45 +08:00
include StickySisFields
are_sis_sticky :name
2011-07-14 00:24:17 +08:00
2011-02-01 09:57:29 +08:00
def wiki
2011-05-03 13:56:39 +08:00
res = self . wiki_id && Wiki . find_by_id ( self . wiki_id )
2011-02-01 09:57:29 +08:00
unless res
res = WikiNamespace . default_for_context ( self ) . wiki
self . wiki_id = res . id if res
self . save
end
res
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def auto_accept? ( user )
return false unless user
2011-09-21 03:24:18 +08:00
self . student_organized? && self . context . users . include? ( user ) &&
self . join_level == 'parent_context_auto_join'
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def allow_join_request? ( user )
return false unless user
2011-09-21 03:24:18 +08:00
self . student_organized? && self . context . users . include? ( user ) &&
[ 'parent_context_auto_join' , 'parent_context_request' ] . include? ( self . join_level )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-10-08 07:19:20 +08:00
def allow_self_signup? ( user )
return false unless user && self . group_category
self . group_category . unrestricted_self_signup? ||
( self . group_category . restricted_self_signup? && self . has_common_section_with_user? ( user ) )
end
def free_association? ( user )
allow_join_request? ( user ) || allow_self_signup? ( user )
end
2012-01-04 04:30:49 +08:00
2012-03-09 01:14:57 +08:00
def participants ( include_observers = false )
# argument needed because #participants is polymorphic for contexts
2011-02-01 09:57:29 +08:00
participating_users . uniq
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def context_code
raise " DONT USE THIS, use .short_name instead " unless ENV [ 'RAILS_ENV' ] == " production "
end
2012-01-04 04:30:49 +08:00
def appointment_context_codes
{ :primary = > [ context_string ] , :secondary = > [ group_category . asset_string ] }
end
2011-02-01 09:57:29 +08:00
def membership_for_user ( user )
self . group_memberships . find_by_user_id ( user && user . id )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def short_name
name
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . find_all_by_context_code ( codes )
ids = codes . map { | c | c . match ( / \ Agroup_( \ d+) \ z / ) [ 1 ] rescue nil } . compact
Group . find ( ids )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
workflow do
state :available do
event :complete , :transitions_to = > :completed
event :close , :transitions_to = > :closed
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# Closed to new entrants
state :closed do
event :complete , :transitions_to = > :completed
event :open , :transitions_to = > :available
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
state :completed
state :deleted
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def active?
self . available? || self . closed?
end
alias_method :destroy! , :destroy
def destroy
self . workflow_state = 'deleted'
self . deleted_at = Time . now
self . save
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def close_memberships_if_deleted
return unless self . deleted?
memberships = self . group_memberships
2011-08-31 06:13:41 +08:00
User . update_all ( { :updated_at = > Time . now . utc } , { :id = > memberships . map ( & :user_id ) . uniq } )
2011-02-01 09:57:29 +08:00
GroupMembership . update_all ( { :workflow_state = > 'deleted' } , { :id = > memberships . map ( & :id ) . uniq } )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
named_scope :active , :conditions = > [ 'groups.workflow_state != ?' , 'deleted' ]
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def full_name
2011-06-14 05:33:59 +08:00
res = before_label ( self . name ) + " "
res += ( self . context . course_code rescue self . context . name ) if self . context
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def is_public
false
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def to_atom
Atom :: Entry . new do | entry |
entry . title = self . name
entry . updated = self . updated_at
entry . published = self . created_at
2012-01-04 04:30:49 +08:00
entry . links << Atom :: Link . new ( :rel = > 'alternate' ,
2011-02-01 09:57:29 +08:00
:href = > " /groups/ #{ self . id } " )
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def add_user ( user )
return nil if ! user
unless member = self . group_memberships . find_by_user_id ( user . id )
member = self . group_memberships . create ( :user = > user )
end
return member
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def invite_user ( user )
return nil if ! user
res = nil
Group . transaction do
2011-05-14 00:49:23 +08:00
res = self . group_memberships . find_by_user_id ( user . id )
unless res
res = self . group_memberships . build ( :user = > user )
res . workflow_state = 'invited'
res . save
end
2011-02-01 09:57:29 +08:00
end
res
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def request_user ( user )
return nil if ! user
res = nil
Group . transaction do
2011-05-14 00:49:23 +08:00
res = self . group_memberships . find_by_user_id ( user . id )
unless res
res = self . group_memberships . build ( :user = > user )
res . workflow_state = 'requested'
res . save
end
2011-02-01 09:57:29 +08:00
end
res
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def invitees = ( params )
invitees = [ ]
( params || { } ) . each do | key , val |
if self . context
invitees << self . context . users . find_by_id ( key . to_i ) if val != '0'
else
invitees << User . find_by_id ( key . to_i ) if val != '0'
end
end
invitees . compact . map { | i | self . invite_user ( i ) } . compact
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def peer_groups
2011-09-21 03:24:18 +08:00
return [ ] if ! self . context || self . student_organized?
2011-09-22 04:31:36 +08:00
category = self . group_category || GroupCategory . student_organized_for ( self . context )
return [ ] unless category
category . groups . find ( :all , :conditions = > [ " id != ? " , self . id ] )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def migrate_content_links ( html , from_course )
Course . migrate_content_links ( html , from_course , self )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
attr_accessor :merge_mappings
attr_accessor :merge_results
def merge_mapped_id ( * args )
nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def map_merge ( * args )
end
def log_merge_result ( text )
@merge_results || = [ ]
@merge_results << text
end
def warn_merge_result ( text )
record_merge_result ( text )
end
2011-09-21 03:24:18 +08:00
def student_organized?
2011-09-22 04:31:36 +08:00
self . group_category && self . group_category . student_organized?
2011-09-21 03:24:18 +08:00
end
2011-02-01 09:57:29 +08:00
def ensure_defaults
2011-04-15 06:09:37 +08:00
self . name || = AutoHandle . generate_securish_uuid
self . uuid || = AutoHandle . generate_securish_uuid
2011-09-22 04:31:36 +08:00
self . group_category || = GroupCategory . student_organized_for ( self . context )
2011-02-01 09:57:29 +08:00
self . join_level || = 'invitation_only'
if self . context && self . context . is_a? ( Course )
2011-09-22 04:31:36 +08:00
self . account = self . context . account
2011-02-01 09:57:29 +08:00
elsif self . context && self . context . is_a? ( Account )
self . account = self . context
end
end
private :ensure_defaults
2011-03-29 03:38:23 +08:00
2011-06-03 23:54:00 +08:00
# update root account when account changes
def account = ( new_account )
self . account_id = new_account . id
end
def account_id = ( new_account_id )
write_attribute ( :account_id , new_account_id )
if self . account_id_changed?
2011-12-28 05:57:56 +08:00
self . root_account = self . account ( true ) . try ( :root_account )
2011-06-03 23:54:00 +08:00
end
end
2011-03-29 03:38:23 +08:00
# if you modify this set_policy block, note that we've denormalized this
# permission check for efficiency -- see User#cached_contexts
2011-02-01 09:57:29 +08:00
set_policy do
2011-03-02 01:44:41 +08:00
given { | user | user && self . participating_group_memberships . find_by_user_id ( user . id ) }
2011-07-14 00:24:17 +08:00
can :read and can :read_roster and can :manage and can :manage_content and can :manage_students and can :manage_admin_users and
2011-02-01 09:57:29 +08:00
can :manage_files and can :moderate_forum and
can :post_to_forum and
can :send_messages and can :create_conferences and
can :create_collaborations and can :read_roster and
can :manage_calendar and
2011-05-26 00:38:32 +08:00
can :update and can :delete and can :create and
2011-07-14 00:24:17 +08:00
can :manage_wiki
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given { | user | user && self . invited_users . include? ( user ) }
2011-07-14 00:24:17 +08:00
can :read
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . context && self . context . grants_right? ( user , session , :participate_as_student ) && self . context . allow_student_organized_groups }
2011-07-14 00:24:17 +08:00
can :create
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . context && self . context . grants_right? ( user , session , :manage_groups ) }
2012-01-17 06:44:10 +08:00
can :read and can :read_roster and can :manage and can :manage_content and can :manage_students and can :manage_admin_users and can :update and can :delete and can :create and can :moderate_forum and can :post_to_forum and can :manage_wiki and can :manage_files and can :create_conferences
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . context && self . context . grants_right? ( user , session , :view_group_pages ) }
2011-07-14 00:24:17 +08:00
can :read and can :read_roster
2012-01-04 04:30:49 +08:00
2011-10-14 07:25:45 +08:00
given { | user , session | self . context && self . free_association? ( user ) }
can :read_roster
2011-02-01 09:57:29 +08:00
end
2011-03-02 01:44:41 +08:00
2011-02-01 09:57:29 +08:00
def file_structure_for ( user )
User . file_structure_for ( self , user )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def is_a_context?
true
end
def members_json_cached
Rails . cache . fetch ( [ 'group_members_json' , self ] . cache_key ) do
2011-10-29 07:19:11 +08:00
self . users . map { | u | u . group_member_json ( self . context ) }
2011-02-01 09:57:29 +08:00
end
end
def members_count_cached
Rails . cache . fetch ( [ 'group_members_count' , self ] . cache_key ) do
self . members_json_cached . length
end
end
2012-02-16 05:40:13 +08:00
def participating_users_count
self . participating_users . count
end
2011-10-21 03:06:55 +08:00
def quota
self . storage_quota || Setting . get_cached ( 'group_default_quota' , 50 . megabytes . to_s ) . to_i
end
2011-02-01 09:57:29 +08:00
TAB_HOME = 0
TAB_PAGES = 1
TAB_PEOPLE = 2
TAB_DISCUSSIONS = 3
TAB_CHAT = 4
TAB_FILES = 5
2011-08-05 04:20:21 +08:00
TAB_CONFERENCES = 6
2011-09-09 06:11:46 +08:00
TAB_ANNOUNCEMENTS = 7
2011-02-01 09:57:29 +08:00
def tabs_available ( user = nil , opts = { } )
available_tabs = [
2011-09-09 06:11:46 +08:00
{ :id = > TAB_HOME , :label = > t ( " # group.tabs.home " , " Home " ) , :css_class = > 'home' , :href = > :group_path } ,
{ :id = > TAB_ANNOUNCEMENTS , :label = > t ( '#tabs.announcements' , " Announcements " ) , :css_class = > 'announcements' , :href = > :group_announcements_path } ,
{ :id = > TAB_PAGES , :label = > t ( " # group.tabs.pages " , " Pages " ) , :css_class = > 'pages' , :href = > :group_wiki_pages_path } ,
{ :id = > TAB_PEOPLE , :label = > t ( " # group.tabs.people " , " People " ) , :css_class = > 'peopel' , :href = > :group_users_path } ,
{ :id = > TAB_DISCUSSIONS , :label = > t ( " # group.tabs.discussions " , " Discussions " ) , :css_class = > 'discussions' , :href = > :group_discussion_topics_path } ,
{ :id = > TAB_CHAT , :label = > t ( " # group.tabs.chat " , " Chat " ) , :css_class = > 'chat' , :href = > :group_chat_path } ,
{ :id = > TAB_FILES , :label = > t ( " # group.tabs.files " , " Files " ) , :css_class = > 'files' , :href = > :group_files_path }
2011-02-01 09:57:29 +08:00
]
2012-01-17 06:44:10 +08:00
available_tabs << { :id = > TAB_CONFERENCES , :label = > t ( '#tabs.conferences' , " Conferences " ) , :css_class = > 'conferences' , :href = > :group_conferences_path } if user && self . grants_right? ( user , nil , :read )
2011-08-05 04:20:21 +08:00
available_tabs
2011-02-01 09:57:29 +08:00
end
def self . serialization_excludes ; [ :uuid ] ; end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . process_migration ( data , migration )
groups = data [ 'groups' ] ? data [ 'groups' ] : [ ]
to_import = migration . to_import 'groups'
groups . each do | group |
if group [ 'migration_id' ] && ( ! to_import || to_import [ group [ 'migration_id' ] ] )
2011-06-18 00:58:18 +08:00
begin
import_from_migration ( group , migration . context )
rescue
migration . add_warning ( " Couldn't import group \" #{ group [ :title ] } \" " , $! )
end
2011-02-01 09:57:29 +08:00
end
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . import_from_migration ( hash , context , item = nil )
hash = hash . with_indifferent_access
return nil if hash [ :migration_id ] && hash [ :groups_to_import ] && ! hash [ :groups_to_import ] [ hash [ :migration_id ] ]
item || = find_by_context_id_and_context_type_and_id ( context . id , context . class . to_s , hash [ :id ] )
item || = find_by_context_id_and_context_type_and_migration_id ( context . id , context . class . to_s , hash [ :migration_id ] ) if hash [ :migration_id ]
item || = context . groups . new
context . imported_migration_items << item if context . imported_migration_items && item . new_record?
item . migration_id = hash [ :migration_id ]
item . name = hash [ :title ]
2011-09-22 04:31:36 +08:00
item . group_category = hash [ :group_category ] . present? ?
context . group_categories . find_or_initialize_by_name ( hash [ :group_category ] ) :
GroupCategory . imported_for ( context )
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
item . save!
context . imported_migration_items << item
item
end
2011-09-08 23:06:51 +08:00
def allow_media_comments?
true
end
2011-09-21 00:51:50 +08:00
def group_category_name
self . read_attribute ( :category )
end
2011-09-22 04:31:36 +08:00
def maintain_category_attribute
# keep this field up to date even though it's not used (group_category_name
# exists solely for the migration that introduces the GroupCategory model).
# this way group_category_name is correct if someone mistakenly uses it
# (modulo category renaming in the GroupCategory model).
self . write_attribute ( :category , self . group_category && self . group_category . name )
2011-09-21 00:51:50 +08:00
end
def as_json ( options = nil )
json = super ( options )
2011-09-22 04:31:36 +08:00
if json && json [ 'group' ]
# remove anything coming automatically from deprecated db column
json [ 'group' ] . delete ( 'category' )
if self . group_category
2012-01-04 04:30:49 +08:00
# put back version from association
2011-09-22 04:31:36 +08:00
json [ 'group' ] [ 'group_category' ] = self . group_category . name
end
end
2011-09-21 00:51:50 +08:00
json
end
2011-10-08 07:19:20 +08:00
def has_common_section?
self . context && self . context . is_a? ( Course ) &&
self . context . course_sections . active . any? { | section | section . common_to_users? ( self . users ) }
end
def has_common_section_with_user? ( user )
return false unless self . context && self . context . is_a? ( Course )
users = self . users + [ user ]
self . context . course_sections . active . any? { | section | section . common_to_users? ( users ) }
end
2011-02-01 09:57:29 +08:00
end