canvas-lms/app/models/group.rb

453 lines
17 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/>.
#
class Group < ActiveRecord::Base
include Context
include Workflow
attr_accessible :name, :context, :max_membership, :group_category, :join_level, :default_view
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
belongs_to :group_category
2011-02-01 09:57:29 +08:00
belongs_to :account
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']
has_many :attachments, :as => :context, :dependent => :destroy, :extend => Attachment::FindInContextAssociation
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'
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 :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
has_many :zip_file_imports, :as => :context
before_save :ensure_defaults, :maintain_category_attribute
2011-02-01 09:57:29 +08:00
after_save :close_memberships_if_deleted
include StickySisFields
are_sis_sticky :name
alias_method :participating_users_association, :participating_users
def participating_users(user_ids = nil)
user_ids ?
participating_users_association.scoped(:conditions => {:id => user_ids}) :
participating_users_association
end
2011-02-01 09:57:29 +08:00
def wiki
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
2011-02-01 09:57:29 +08:00
def auto_accept?(user)
student view; closes #6995 allows course admins to view the course from a student perspective. this is accessible from a button on the course/settings page. They should be able to interact with the course as a student would, including submitting homework and quizzes. Right now there is one student view student per course, so if the course has multiple administrators, they will all share the same student view student. There are a few things that won't work in student view the way the would for a normal student, most notably access to conversations is disabled. Additionally, any publicly visible action that the teacher takes while in student view will still be publicly visible -- for example if the teacher posts a discussion topic/reply as the student view student, it will be visible to the whole class. test-plan: - (the following should be tried both as a full teacher and as a section-limited course admin) - set up a few assignments, quizzes, discussions, and module progressions in a course. - enter student view from the coures settings page. - work through the things you set up above. - leave student view from the upper right corner of the page. - as a teacher you should be able to grade the fake student so that they can continue to progress. - the student should not show up in the course users list - the student should not show up at the account level at all: * total user list * statistics Change-Id: I886a4663777f3ef2bdae594349ff6da6981e14ed Reviewed-on: https://gerrit.instructure.com/9484 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com>
2012-03-14 04:08:19 +08:00
self.context.grants_right?(user, :participate_in_groups) &&
self.student_organized? &&
self.join_level == 'parent_context_auto_join'
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def allow_join_request?(user)
student view; closes #6995 allows course admins to view the course from a student perspective. this is accessible from a button on the course/settings page. They should be able to interact with the course as a student would, including submitting homework and quizzes. Right now there is one student view student per course, so if the course has multiple administrators, they will all share the same student view student. There are a few things that won't work in student view the way the would for a normal student, most notably access to conversations is disabled. Additionally, any publicly visible action that the teacher takes while in student view will still be publicly visible -- for example if the teacher posts a discussion topic/reply as the student view student, it will be visible to the whole class. test-plan: - (the following should be tried both as a full teacher and as a section-limited course admin) - set up a few assignments, quizzes, discussions, and module progressions in a course. - enter student view from the coures settings page. - work through the things you set up above. - leave student view from the upper right corner of the page. - as a teacher you should be able to grade the fake student so that they can continue to progress. - the student should not show up in the course users list - the student should not show up at the account level at all: * total user list * statistics Change-Id: I886a4663777f3ef2bdae594349ff6da6981e14ed Reviewed-on: https://gerrit.instructure.com/9484 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com>
2012-03-14 04:08:19 +08:00
self.context.grants_right?(user, :participate_in_groups) &&
self.student_organized? &&
['parent_context_auto_join', 'parent_context_request'].include?(self.join_level)
2011-02-01 09:57:29 +08:00
end
def allow_self_signup?(user)
student view; closes #6995 allows course admins to view the course from a student perspective. this is accessible from a button on the course/settings page. They should be able to interact with the course as a student would, including submitting homework and quizzes. Right now there is one student view student per course, so if the course has multiple administrators, they will all share the same student view student. There are a few things that won't work in student view the way the would for a normal student, most notably access to conversations is disabled. Additionally, any publicly visible action that the teacher takes while in student view will still be publicly visible -- for example if the teacher posts a discussion topic/reply as the student view student, it will be visible to the whole class. test-plan: - (the following should be tried both as a full teacher and as a section-limited course admin) - set up a few assignments, quizzes, discussions, and module progressions in a course. - enter student view from the coures settings page. - work through the things you set up above. - leave student view from the upper right corner of the page. - as a teacher you should be able to grade the fake student so that they can continue to progress. - the student should not show up in the course users list - the student should not show up at the account level at all: * total user list * statistics Change-Id: I886a4663777f3ef2bdae594349ff6da6981e14ed Reviewed-on: https://gerrit.instructure.com/9484 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com>
2012-03-14 04:08:19 +08:00
self.context.grants_right?(user, :participate_in_groups) &&
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
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
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
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
2011-02-01 09:57:29 +08:00
def short_name
name
end
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
2011-02-01 09:57:29 +08:00
workflow do
state :available do
event :complete, :transitions_to => :completed
event :close, :transitions_to => :closed
end
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
2011-02-01 09:57:29 +08:00
state :completed
state :deleted
end
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
2011-02-01 09:57:29 +08:00
def close_memberships_if_deleted
return unless self.deleted?
memberships = self.group_memberships
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
2011-02-01 09:57:29 +08:00
named_scope :active, :conditions => ['groups.workflow_state != ?', 'deleted']
2011-02-01 09:57:29 +08:00
def full_name
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
2011-02-01 09:57:29 +08:00
def is_public
false
end
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
entry.links << Atom::Link.new(:rel => 'alternate',
2011-02-01 09:57:29 +08:00
:href => "/groups/#{self.id}")
end
end
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
2011-02-01 09:57:29 +08:00
def invite_user(user)
return nil if !user
res = nil
Group.transaction do
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
2011-02-01 09:57:29 +08:00
def request_user(user)
return nil if !user
res = nil
Group.transaction do
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
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
2011-02-01 09:57:29 +08:00
def peer_groups
return [] if !self.context || self.student_organized?
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
2011-02-01 09:57:29 +08:00
def migrate_content_links(html, from_course)
Course.migrate_content_links(html, from_course, self)
end
2011-02-01 09:57:29 +08:00
attr_accessor :merge_mappings
attr_accessor :merge_results
def merge_mapped_id(*args)
nil
end
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
def student_organized?
self.group_category && self.group_category.student_organized?
end
2011-02-01 09:57:29 +08:00
def ensure_defaults
self.name ||= AutoHandle.generate_securish_uuid
self.uuid ||= AutoHandle.generate_securish_uuid
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)
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
# 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?
self.root_account = self.account(true).try(:root_account)
end
end
# 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
given { |user| user && self.participating_group_memberships.find_by_user_id(user.id) }
can :read and can :read_roster and can :manage and can :manage_content and can :manage_students and can :manage_admin_users and
can :manage_files and
2011-02-01 09:57:29 +08:00
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
can :update and can :delete and can :create and
can :manage_wiki
# if I am a member of this group and I can moderate_forum in the group's context
# (makes it so group members cant edit each other's discussion entries)
given { |user, session| user && self.participating_group_memberships.find_by_user_id(user.id) && (!self.context || self.context.grants_right?(user, session, :moderate_forum)) }
can :moderate_forum
2011-02-01 09:57:29 +08:00
given { |user| user && self.invited_users.include?(user) }
can :read
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 }
can :create
2011-02-01 09:57:29 +08:00
given { |user, session| self.context && self.context.grants_right?(user, session, :manage_groups) }
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
2011-02-01 09:57:29 +08:00
given { |user, session| self.context && self.context.grants_right?(user, session, :view_group_pages) }
can :read and can :read_roster
given { |user, session| self.context && self.free_association?(user) }
can :read_roster
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def file_structure_for(user)
User.file_structure_for(self, user)
end
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
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
def participating_users_count
self.participating_users.count
end
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
TAB_CONFERENCES = 6
TAB_ANNOUNCEMENTS = 7
2011-02-01 09:57:29 +08:00
def tabs_available(user=nil, opts={})
available_tabs = [
{ :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
]
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)
available_tabs
2011-02-01 09:57:29 +08:00
end
def self.serialization_excludes; [:uuid]; end
2011-02-01 09:57:29 +08:00
def self.process_migration(data, migration)
groups = data['groups'] ? data['groups']: []
groups.each do |group|
if migration.import_object?("groups", group['migration_id'])
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
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]
item.group_category = hash[:group_category].present? ?
context.group_categories.find_or_initialize_by_name(hash[:group_category]) :
GroupCategory.imported_for(context)
2011-02-01 09:57:29 +08:00
item.save!
context.imported_migration_items << item
item
end
def allow_media_comments?
true
end
def group_category_name
self.read_attribute(:category)
end
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)
end
def as_json(options=nil)
json = super(options)
if json && json['group']
# remove anything coming automatically from deprecated db column
json['group'].delete('category')
if self.group_category
# put back version from association
json['group']['group_category'] = self.group_category.name
end
end
json
end
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