canvas-lms/app/models/group.rb

701 lines
26 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 'atom'
2011-02-01 09:57:29 +08:00
class Group < ActiveRecord::Base
include Context
include Workflow
include CustomValidations
group leaders fixes CNVS-11833 test plan - regression test teacher view of groups page a group can only have one group leader, and that user must be in the group. anything that can be done to remove a user from a group should revoke their leadership if they have it. a user's leadership should not be revoked unless the teacher revokes it, a different leader is chosen, or that user leaves the group. a leader's user should have a user icon on it and their name should appear next to the group's name a few test cases: - set group leader using gear menu on user in group - revoke the leadership of the user using the gear menu - ensure that the group is now leaderless - set a group leader - set a different user as leader - ensure that a screenreader identifies the group leader link as "Group Leader" - ensure that the first user is no longer the leader - set a group leader - remove that user from the group by dragging and dropping - ensure that the user is no longer the group leader - set a group leader - move the user to another group using the option on the gear menu - ensure that the user is not the leader of their original or new group - in a large-roster course, add users to a group user the plus button next to the gear menu - set a group leader using the gear menu next to a user - fill a group up to it's maximum limit of students - ensure that the "full" label shows up for that group - ensure that when you narrow the browser size, you can still tell that the group is full Change-Id: I8bb1b62e0f36a37a24e050878c945f822fe9f66c Reviewed-on: https://gerrit.instructure.com/34360 Reviewed-by: Ethan Vizitei <evizitei@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
2014-05-14 09:13:18 +08:00
attr_accessible :name, :context, :max_membership, :group_category, :join_level, :default_view, :description, :is_public, :avatar_attachment, :storage_quota_mb, :leader
validates_presence_of :context_id, :context_type, :account_id, :root_account_id, :workflow_state
validates_allowed_transitions :is_public, false => true
2011-02-01 09:57:29 +08:00
# use to skip queries in can_participate?, called by policy block
attr_accessor :can_participate
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
belongs_to :context, :polymorphic => true
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account']
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 :collaborators
2011-02-01 09:57:29 +08:00
has_many :external_feeds, :as => :context, :dependent => :destroy
has_many :messages, :as => :context, :dependent => :destroy
belongs_to :wiki
has_many :web_conferences, :as => :context, :dependent => :destroy
has_many :collaborations, :as => :context, :order => 'title, created_at', :dependent => :destroy
has_many :media_objects, :as => :context
has_many :zip_file_imports, :as => :context
has_many :content_migrations, :as => :context
zip content exports for course, group, user test plan: 1. use the content exports api with export_type=zip to export files from courses, groups, and users a. confirm only users who have permission to download files from these contexts can perform the export b. confirm that deleted files and folders do not show up in the downloaded archive c. confirm that students cannot download locked files or folders from courses this way d. check the progress endpoint and make sure it increments sanely 2. perform selective content exports by passing an array of ids in select[folders] and/or select[attachments]. for example, ?select[folders][]=123&select[folders][]=456 ?select[attachments][]=345 etc. a. any selected files, plus the full contents of any selected folders (that the caller has permission to see) should be included - that means locked files and subfolders should be excluded from the archive b. if all selected files and folders are descendants of the same subfolder X, the export should be named "X_export.zip" and all paths inside the zip should be relative to it. for example, if you are exporting A/B/1 and A/C/2, you should get "A_export.zip" containing files "B/1" and "C/2". 3. use the index and show endpoints to list and view content exports in courses, groups, and users a. confirm students cannot view non-zip course exports (such as common cartridge exports) b. confirm students cannot view other users' file (zip) exports, in course, group, and user context c. confirm teachers cannot view other users' file (zip) exports, in course, group, and user context (but can still view course [cc] exports initiated by other teachers) 4. look at /courses/X/content_exports (web, not API) a. confirm teachers see file exports they performed b. confirm teachers do not see file exports performed by other teachers c. confirm teachers see all non-zip course exports (cc/qti) including those initiated by other teachers 5. as a site admin user, perform a zip export of another user's files. then, as that other user, go to /dashboard/data_exports and confirm that the export performed by the site admin user is not shown. fixes CNVS-12706 Change-Id: Ie9b58e44ac8006a9c9171b3ed23454bf135385b0 Reviewed-on: https://gerrit.instructure.com/34341 Reviewed-by: James Williams <jamesw@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Jon Willesen <jonw@instructure.com>
2014-07-18 04:00:32 +08:00
has_many :content_exports, :as => :context
has_many :usage_rights, as: :context, class_name: 'UsageRights', dependent: :destroy
belongs_to :avatar_attachment, :class_name => "Attachment"
group leaders fixes CNVS-11833 test plan - regression test teacher view of groups page a group can only have one group leader, and that user must be in the group. anything that can be done to remove a user from a group should revoke their leadership if they have it. a user's leadership should not be revoked unless the teacher revokes it, a different leader is chosen, or that user leaves the group. a leader's user should have a user icon on it and their name should appear next to the group's name a few test cases: - set group leader using gear menu on user in group - revoke the leadership of the user using the gear menu - ensure that the group is now leaderless - set a group leader - set a different user as leader - ensure that a screenreader identifies the group leader link as "Group Leader" - ensure that the first user is no longer the leader - set a group leader - remove that user from the group by dragging and dropping - ensure that the user is no longer the group leader - set a group leader - move the user to another group using the option on the gear menu - ensure that the user is not the leader of their original or new group - in a large-roster course, add users to a group user the plus button next to the gear menu - set a group leader using the gear menu next to a user - fill a group up to it's maximum limit of students - ensure that the "full" label shows up for that group - ensure that when you narrow the browser size, you can still tell that the group is full Change-Id: I8bb1b62e0f36a37a24e050878c945f822fe9f66c Reviewed-on: https://gerrit.instructure.com/34360 Reviewed-by: Ethan Vizitei <evizitei@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
2014-05-14 09:13:18 +08:00
belongs_to :leader, :class_name => "User"
EXPORTABLE_ATTRIBUTES = [
:id, :name, :workflow_state, :created_at, :updated_at, :context_id, :context_type, :category, :max_membership, :hashtag, :show_public_context_messages, :is_public,
:account_id, :default_wiki_editing_roles, :wiki_id, :deleted_at, :join_level, :default_view, :storage_quota, :uuid, :root_account_id, :sis_source_id, :sis_batch_id,
:group_category_id, :description, :avatar_attachment_id
]
EXPORTABLE_ASSOCIATIONS = [
:users, :group_memberships, :users, :context, :group_category, :account, :root_account, :calendar_events, :discussion_topics, :discussion_entries, :announcements,
:attachments, :folders, :collaborators, :wiki, :web_conferences, :collaborations, :media_objects, :avatar_attachment
]
before_validation :ensure_defaults
before_save :maintain_category_attribute
before_save :update_max_membership_from_group_category
after_create :refresh_group_discussion_topics
delegate :time_zone, :to => :context
include StickySisFields
are_sis_sticky :name
validates_each :name do |record, attr, value|
if value.blank?
record.errors.add attr, t(:name_required, "Name is required")
elsif value.length > maximum_string_length
record.errors.add attr, t(:name_too_long, "Enter a shorter group name")
end
end
validates_each :max_membership do |record, attr, value|
next if value.nil?
record.errors.add attr, t(:greater_than_1, "Must be greater than 1") unless value.to_i > 1
end
def refresh_group_discussion_topics
if self.group_category
self.group_category.discussion_topics.active.each(&:update_subtopics)
end
end
def includes_user?(user, membership_scope=group_memberships)
return false if user.nil? || user.new_record?
membership_scope.where(user_id: user).exists?
end
alias_method :participating_users_association, :participating_users
def participating_users(user_ids = nil)
user_ids ?
participating_users_association.where(:id =>user_ids) :
participating_users_association
end
def all_real_students
return self.context.all_real_students.where("users.id IN (?)", self.users.pluck(:id)) if self.context.respond_to? "all_real_students"
self.users
end
def wiki_with_create
Wiki.wiki_for_context(self)
2011-02-01 09:57:29 +08:00
end
alias_method_chain :wiki, :create
def auto_accept?
self.group_category &&
self.group_category.allows_multiple_memberships? &&
self.join_level == 'parent_context_auto_join'
2011-02-01 09:57:29 +08:00
end
def allow_join_request?
self.group_category &&
self.group_category.allows_multiple_memberships? &&
['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)
self.group_category &&
(self.group_category.unrestricted_self_signup? ||
(self.group_category.restricted_self_signup? && self.has_common_section_with_user?(user)))
end
def full?
!student_organized? && ((!max_membership && group_category_limit_met?) || (max_membership && participating_users.size >= max_membership))
end
def group_category_limit_met?
group_category && group_category.group_limit && participating_users.size >= group_category.group_limit
end
Automatically Assign group leaders closes: CNVS-11834 This creates a way for an instructor to assign a random student as the group leader. It only applies when an instructor is having groups created automatically at the time of defining a group category. This also take an opportunity to refactor out some bloated code from the group_categories_controller and move it into some separate objects that can be more easily understood and rapidly unit tested through all the necessary permutations (allowing higher level integration tests to just cover a case or two) It ALSO removes group leadership knowledge into it's own object so that the callbacks in other objects are simple and the logic regarding how to do group leadership management is in one place. TEST PLAN: AUTO_DISTRIBUTION: 1) login as an instructor 2) go to the "people" tab and try to create a group set. 3) click on the "Create [0] groups for me" radio button; verify you now have controls for assigning a group leader automatically and that the strategy radio buttons are greyed out. 4) check the "Assign a group leader automatically" checkbox; verify the 2 nested radio buttons for "random" and "first" strategies become enabled 5) select a strategy and fill out the rest of the form, then submit (make sure your background job is running) 6) verify after groups are created that each group has a leader, and that the leader is in fact a member of the group. SelfSignup: 1) login as an instructor 2) go to the "people" tab and try to create a group set. 3) enable self-signup. 4) check the "Assign a group leader automatically" checkbox; verify the 2 nested radio buttons for "random" and "first" strategies become enabled 5) select a strategy and fill out the rest of the form, then submit. 6) Login as a student for the same course and join the group. 7) verify that the student has been made the group leader. Change-Id: I2cdd9f5ed2fd577469beec4ab7369c69ecf7eaa6 Reviewed-on: https://gerrit.instructure.com/35130 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Braden Anderson <banderson@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
2014-05-20 02:18:17 +08:00
private :group_category_limit_met?
def student_organized?
group_category && group_category.student_organized?
end
def update_max_membership_from_group_category
if (!max_membership || max_membership == 0) && group_category && group_category.group_limit
self.max_membership = group_category.group_limit
end
end
def free_association?(user)
auto_accept? || allow_join_request? || allow_self_signup?(user)
end
def allow_student_forum_attachments
context.respond_to?(:allow_student_forum_attachments) && context.allow_student_forum_attachments
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 Rails.env.production?
2011-02-01 09:57:29 +08:00
end
def context_available?
return false unless self.context
case self.context
when Course
self.context.available?
else
true
end
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.where(user_id: user).first if user
2011-02-01 09:57:29 +08:00
end
def has_member?(user)
return nil unless user.present?
if self.group_memberships.loaded?
return self.group_memberships.to_a.find { |gm| gm.accepted? && gm.user_id == user.id }
else
self.participating_group_memberships.where(user_id: user).first
end
end
def has_moderator?(user)
return nil unless user.present?
if self.group_memberships.loaded?
return self.group_memberships.to_a.find { |gm| gm.accepted? && gm.user_id == user.id && gm.moderator }
end
self.participating_group_memberships.moderators.where(user_id: user).first
end
def should_add_creator?(creator)
self.group_category &&
(self.group_category.communities? || (self.group_category.student_organized? && self.context.user_is_student?(creator)))
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
def self.not_in_group_sql_fragment(groups)
return nil if groups.empty?
sanitize_sql([<<-SQL, groups])
NOT EXISTS (SELECT * FROM group_memberships gm
WHERE gm.user_id = users.id AND
gm.workflow_state != 'deleted' AND
gm.group_id IN (?))
SQL
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.utc
2011-02-01 09:57:29 +08:00
self.save
end
Bookmarker = BookmarkedCollection::SimpleBookmarker.new(Group, :name, :id)
scope :active, -> { where("groups.workflow_state<>'deleted'") }
scope :by_name, -> { order(Bookmarker.order_by) }
scope :uncategorized, -> { where("groups.group_category_id IS NULL") }
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
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
# this method is idempotent
def add_user(user, new_record_state=nil, moderator=nil)
2011-02-01 09:57:29 +08:00
return nil if !user
attrs = { :user => user, :moderator => !!moderator }
new_record_state ||= case self.join_level
when 'invitation_only' then 'invited'
when 'parent_context_request' then 'requested'
when 'parent_context_auto_join' then 'accepted'
end
attrs[:workflow_state] = new_record_state if new_record_state
if member = self.group_memberships.where(user_id: user).first
member.workflow_state = new_record_state unless member.active?
# only update moderator if true/false is explicitly passed in
member.moderator = moderator unless moderator.nil?
member.save if member.changed?
else
member = self.group_memberships.create(attrs)
end
# permissions for this user in the group are probably different now
clear_permissions_cache(user)
2011-02-01 09:57:29 +08:00
return member
end
def set_users(users)
user_ids = users.map(&:id)
memberships = []
transaction do
self.group_memberships.where("user_id NOT IN (?)", user_ids).destroy_all
users.each do |user|
memberships << invite_user(user)
end
end
memberships
end
def bulk_add_users_to_group(users, options = {})
return if users.empty?
user_ids = users.map(&:id)
old_group_memberships = self.group_memberships.where("user_id IN (?)", user_ids).all
bulk_insert_group_memberships(users, options)
all_group_memberships = self.group_memberships.where("user_id IN (?)", user_ids)
new_group_memberships = all_group_memberships - old_group_memberships
new_group_memberships.sort_by!(&:user_id)
users.sort_by!(&:id)
users.each {|user| clear_permissions_cache(user) }
if self.context_available?
notification_name = options[:notification_name] || "New Context Group Membership"
notification = BroadcastPolicy.notification_finder.by_name(notification_name)
users.each_with_index do |user, index|
BroadcastPolicy.notifier.send_later_enqueue_args(:send_notification,
{:priority => Delayed::LOW_PRIORITY},
new_group_memberships[index],
notification_name.parameterize.underscore.to_sym,
notification,
[user])
end
end
new_group_memberships
end
def bulk_insert_group_memberships(users, options = {})
current_time = Time.now
options = {
:group_id => self.id,
:workflow_state => 'accepted',
:moderator => false,
:created_at => current_time,
:updated_at => current_time
}.merge(options)
GroupMembership.bulk_insert(users.map{ |user|
options.merge({:user_id => user.id, :uuid => CanvasSlug.generate_securish_uuid})
})
end
2011-02-01 09:57:29 +08:00
def invite_user(user)
self.add_user(user, 'invited')
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def request_user(user)
self.add_user(user, 'requested')
2011-02-01 09:57:29 +08:00
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.where(id: key.to_i).first if val != '0'
2011-02-01 09:57:29 +08:00
else
invitees << User.where(id: key.to_i).first if val != '0'
2011-02-01 09:57:29 +08:00
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.group_category || self.group_category.allows_multiple_memberships?
self.group_category.groups.where("id<>?", self).all
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
2011-02-01 09:57:29 +08:00
def log_merge_result(text)
@merge_results ||= []
@merge_results << text
end
2011-02-01 09:57:29 +08:00
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 ||= CanvasSlug.generate_securish_uuid
self.uuid ||= CanvasSlug.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'
self.is_public ||= false
self.is_public = false unless self.group_category.try(:communities?)
2011-02-01 09:57:29 +08:00
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
# Course-level groups don't grant any permissions unless their containing context can
# be read by the user in question
given { |user, session| self.context.is_a?(Account) || self.context.grants_right?(user, session, :read) }
use_additional_policy do
given { |user| user && self.has_member?(user) }
can :create_collaborations and
can :manage_calendar and
can :manage_content and
can :manage_files and
can :manage_wiki and
can :post_to_forum and
can :read and
can :read_forum and
can :read_roster and
can :send_messages and
can :send_messages_all and
can :view_unpublished_items
# 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.has_member?(user) && (!self.context || self.context.grants_right?(user, session, :moderate_forum)) }
can :moderate_forum
given { |user| user && self.has_moderator?(user) }
can :delete and
can :manage and
can :manage_admin_users and
can :manage_students and
can :moderate_forum and
can :update
given { |user| user && self.leader == user }
can :update
given { |user| self.group_category.try(:communities?) }
can :create
given { |user, session| self.context && self.context.grants_right?(user, session, :participate_as_student) }
can :participate_as_student
given { |user, session| self.grants_right?(user, session, :participate_as_student) && self.context.allow_student_organized_groups }
can :create
given { |user, session| self.context && self.context.grants_right?(user, session, :manage_groups) }
can :create and
can :create_collaborations and
can :delete and
can :manage and
can :manage_admin_users and
can :manage_content and
can :manage_files and
can :manage_students and
can :manage_wiki and
can :moderate_forum and
can :post_to_forum and
can :read and
can :read_forum and
can :read_roster and
can :send_messages and
can :send_messages_all and
can :update and
can :view_unpublished_items
given { |user, session| self.context && self.context.grants_right?(user, session, :view_group_pages) }
can :read and can :read_forum and can :read_roster
# Participate means the user is connected to the group somehow and can be
given { |user| user && can_participate?(user) }
can :participate
# Join is participate + the group being in a state that allows joining directly (free_association)
given { |user| user && can_participate?(user) && free_association?(user)}
can :join and can :read_roster
given { |user| user && (self.group_category.try(:allows_multiple_memberships?) || allow_self_signup?(user)) }
can :leave
given {|user, session| self.grants_right?(user, session, :manage_content) && self.context && self.context.grants_right?(user, session, :create_conferences)}
can :create_conferences
end
end
def users_visible_to(user)
grants_right?(user, :read) ? users : users.none
end
# Helper needed by several permissions, use grants_right?(user, :participate)
def can_participate?(user)
return true if can_participate
return false unless user.present? && self.context.present?
return true if self.group_category.try(:communities?)
if self.context.is_a?(Course)
return self.context.enrollments.not_fake.except(:includes).where(:user_id => user.id).exists?
elsif self.context.is_a?(Account)
return self.context.user_account_associations.where(:user_id => user.id).exists?
end
return false
2011-02-01 09:57:29 +08:00
end
private :can_participate?
def user_can_manage_own_discussion_posts?(user)
return true unless self.context.is_a?(Course)
context.user_can_manage_own_discussion_posts?(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 members_count
self.participating_group_memberships.count
end
def quota
return self.storage_quota || self.account.default_group_storage_quota || self.class.default_storage_quota
end
def self.default_storage_quota
Setting.get('group_default_quota', 50.megabytes.to_s).to_i
end
def storage_quota_mb
quota / 1.megabyte
end
def storage_quota_mb=(val)
self.storage_quota = val.try(:to_i).try(:megabytes)
end
TAB_HOME, TAB_PAGES, TAB_PEOPLE, TAB_DISCUSSIONS, TAB_FILES,
TAB_CONFERENCES, TAB_ANNOUNCEMENTS, TAB_PROFILE, TAB_SETTINGS, TAB_COLLABORATIONS = *1..20
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_path },
{ :id => TAB_PEOPLE, :label => t("#group.tabs.people", "People"), :css_class => 'people', :href => :group_users_path },
{ :id => TAB_DISCUSSIONS, :label => t("#group.tabs.discussions", "Discussions"), :css_class => 'discussions', :href => :group_discussion_topics_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
]
if root_account.try :canvas_network_enabled?
available_tabs << {:id => TAB_PROFILE, :label => t('#tabs.profile', 'Profile'), :css_class => 'profile', :href => :group_profile_path}
end
available_tabs << { :id => TAB_CONFERENCES, :label => t('#tabs.conferences', "Conferences"), :css_class => 'conferences', :href => :group_conferences_path } if user && self.grants_right?(user, :read)
available_tabs << { :id => TAB_COLLABORATIONS, :label => t('#tabs.collaborations', "Collaborations"), :css_class => 'collaborations', :href => :group_collaborations_path } if user && self.grants_right?(user, :read)
if root_account.try(:canvas_network_enabled?) && user && grants_right?(user, :manage)
available_tabs << { :id => TAB_SETTINGS, :label => t('#tabs.settings', 'Settings'), :css_class => 'settings', :href => :edit_group_path }
end
available_tabs
2011-02-01 09:57:29 +08:00
end
def self.serialization_excludes; [:uuid]; 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
def self.join_levels
[
["invitation_only", "Invite only"],
["parent_context_auto_join", "Auto join"],
["parent_context_request", "Request to join"]
]
end
def associated_shards
[Shard.default]
end
# Public: Determine whether a feature is enabled, deferring to the group's context.
#
# Returns a boolean.
def feature_enabled?(feature)
# shouldn't matter, but most specs create anonymous (contextless) groups :(
return false if context.nil?
context.feature_enabled?(feature)
end
Add a permission option in the api to return if the user can create topics. closes CNVS-6824 This adds a permissions attribute to the returned json for discussion topic contexts (Course, Group). The permissions attribute contains an optional permission of "create_discission_topic" which returns true or false depending on whether the current user can create discussion topics for the course or group. For performance reasons this is only added to a single course/group json and not in lists so the only call that will return it is /api/v1/<context>/<context_id> where context is a course or group. Since we did not want to include this on every response its a custom permissions attribute for course and groups in the course_json or group_json serialization methods. Using the includes parameter for the API supplying a value of "permissions" will include the permissions with "create_discussion_topic" for a group and course. When the object is serialized to json it checked to see if the model implements a serialize_permissions method and calls that to render or override permissions generated from the policies. - Create a test Course. Make sure the course allows members to post topics. - Add a student to the course. - Make a call to /api/v1/courses/<id> where "<id>" is the id of the created course. - The response should include a permissions attribute with a boolean value for "create_discission_topic" see the example below. - Make a call to /api/v1/courses to return a list of course objects. - The permissions attribute should not be included in the response. - Create a test Group tied to the course created in the first step. - Add a mamber to the group. - Make a call to /api/v1/groups/<id> where "<id>" is the id of the created group. - The response should include a permissions attribute with a boolean value for "create_discission_topic" see the example below. - Make a call to /api/v1/groups to return a list of group objects. - The permissions attribute should not be included in the response. Example Response: { id: 42, ... permissions: { create_discission_topic: true } } Change-Id: Ia02d5aa67e345740a93dd0f63e357e7cb5e1efd6 Reviewed-on: https://gerrit.instructure.com/24478 Reviewed-by: Jacob Fugal <jacob@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: August Thornton <august@instructure.com> Product-Review: Nick Cloward <ncloward@instructure.com>
2013-09-18 06:24:57 +08:00
def serialize_permissions(permissions_hash, user, session)
permissions_hash.merge(
create_discussion_topic: DiscussionTopic.context_allows_user_to_create?(self, user, session),
create_announcement: Announcement.context_allows_user_to_create?(self, user, session)
Add a permission option in the api to return if the user can create topics. closes CNVS-6824 This adds a permissions attribute to the returned json for discussion topic contexts (Course, Group). The permissions attribute contains an optional permission of "create_discission_topic" which returns true or false depending on whether the current user can create discussion topics for the course or group. For performance reasons this is only added to a single course/group json and not in lists so the only call that will return it is /api/v1/<context>/<context_id> where context is a course or group. Since we did not want to include this on every response its a custom permissions attribute for course and groups in the course_json or group_json serialization methods. Using the includes parameter for the API supplying a value of "permissions" will include the permissions with "create_discussion_topic" for a group and course. When the object is serialized to json it checked to see if the model implements a serialize_permissions method and calls that to render or override permissions generated from the policies. - Create a test Course. Make sure the course allows members to post topics. - Add a student to the course. - Make a call to /api/v1/courses/<id> where "<id>" is the id of the created course. - The response should include a permissions attribute with a boolean value for "create_discission_topic" see the example below. - Make a call to /api/v1/courses to return a list of course objects. - The permissions attribute should not be included in the response. - Create a test Group tied to the course created in the first step. - Add a mamber to the group. - Make a call to /api/v1/groups/<id> where "<id>" is the id of the created group. - The response should include a permissions attribute with a boolean value for "create_discission_topic" see the example below. - Make a call to /api/v1/groups to return a list of group objects. - The permissions attribute should not be included in the response. Example Response: { id: 42, ... permissions: { create_discission_topic: true } } Change-Id: Ia02d5aa67e345740a93dd0f63e357e7cb5e1efd6 Reviewed-on: https://gerrit.instructure.com/24478 Reviewed-by: Jacob Fugal <jacob@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: August Thornton <august@instructure.com> Product-Review: Nick Cloward <ncloward@instructure.com>
2013-09-18 06:24:57 +08:00
)
end
zip content exports for course, group, user test plan: 1. use the content exports api with export_type=zip to export files from courses, groups, and users a. confirm only users who have permission to download files from these contexts can perform the export b. confirm that deleted files and folders do not show up in the downloaded archive c. confirm that students cannot download locked files or folders from courses this way d. check the progress endpoint and make sure it increments sanely 2. perform selective content exports by passing an array of ids in select[folders] and/or select[attachments]. for example, ?select[folders][]=123&select[folders][]=456 ?select[attachments][]=345 etc. a. any selected files, plus the full contents of any selected folders (that the caller has permission to see) should be included - that means locked files and subfolders should be excluded from the archive b. if all selected files and folders are descendants of the same subfolder X, the export should be named "X_export.zip" and all paths inside the zip should be relative to it. for example, if you are exporting A/B/1 and A/C/2, you should get "A_export.zip" containing files "B/1" and "C/2". 3. use the index and show endpoints to list and view content exports in courses, groups, and users a. confirm students cannot view non-zip course exports (such as common cartridge exports) b. confirm students cannot view other users' file (zip) exports, in course, group, and user context c. confirm teachers cannot view other users' file (zip) exports, in course, group, and user context (but can still view course [cc] exports initiated by other teachers) 4. look at /courses/X/content_exports (web, not API) a. confirm teachers see file exports they performed b. confirm teachers do not see file exports performed by other teachers c. confirm teachers see all non-zip course exports (cc/qti) including those initiated by other teachers 5. as a site admin user, perform a zip export of another user's files. then, as that other user, go to /dashboard/data_exports and confirm that the export performed by the site admin user is not shown. fixes CNVS-12706 Change-Id: Ie9b58e44ac8006a9c9171b3ed23454bf135385b0 Reviewed-on: https://gerrit.instructure.com/34341 Reviewed-by: James Williams <jamesw@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Jon Willesen <jonw@instructure.com>
2014-07-18 04:00:32 +08:00
def content_exports_visible_to(user)
self.content_exports.where(user_id: user)
end
def account_chain
@account_chain ||= Account.account_chain(account_id)
@account_chain.dup
end
def sortable_name
name
end
2011-02-01 09:57:29 +08:00
end