2011-02-01 09:57:29 +08:00
#
2013-10-24 06:29:14 +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/>.
#
2015-04-09 01:21:08 +08:00
require 'atom'
2011-02-01 09:57:29 +08:00
class Group < ActiveRecord :: Base
include Context
include Workflow
2012-05-30 06:55:40 +08:00
include CustomValidations
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
2013-08-08 06:19:48 +08:00
validates_presence_of :context_id , :context_type , :account_id , :root_account_id , :workflow_state
2012-06-07 01:51:49 +08:00
validates_allowed_transitions :is_public , false = > true
2011-02-01 09:57:29 +08:00
2014-02-28 07:36:27 +08:00
# use to skip queries in can_participate?, called by policy block
attr_accessor :can_participate
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
belongs_to :context , :polymorphic = > true
2014-05-09 01:49:10 +08:00
validates_inclusion_of :context_type , :allow_nil = > true , :in = > [ 'Course' , 'Account' ]
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'
2012-12-19 05:59:09 +08:00
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
2011-12-10 06:37:12 +08:00
has_many :zip_file_imports , :as = > :context
2014-04-23 01:55:08 +08:00
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
2014-11-14 02:35:24 +08:00
has_many :usage_rights , as : :context , class_name : 'UsageRights' , dependent : :destroy
2012-06-02 00:24:41 +08:00
belongs_to :avatar_attachment , :class_name = > " Attachment "
2014-05-14 09:13:18 +08:00
belongs_to :leader , :class_name = > " User "
2012-01-04 04:30:49 +08:00
2014-04-11 03:03:43 +08:00
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 ,
2014-05-21 05:36:13 +08:00
:attachments , :folders , :collaborators , :wiki , :web_conferences , :collaborations , :media_objects , :avatar_attachment
2014-04-11 03:03:43 +08:00
]
2013-08-08 06:19:48 +08:00
before_validation :ensure_defaults
before_save :maintain_category_attribute
2014-12-11 09:15:54 +08:00
before_save :update_max_membership_from_group_category
2012-01-04 04:30:49 +08:00
2015-02-05 01:43:13 +08:00
after_create :refresh_group_discussion_topics
2014-05-14 07:48:56 +08:00
delegate :time_zone , :to = > :context
2011-09-22 01:36:45 +08:00
include StickySisFields
are_sis_sticky :name
2011-07-14 00:24:17 +08:00
2013-10-16 04:18:38 +08:00
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
2013-07-23 23:15:22 +08:00
2014-04-23 01:37:54 +08:00
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
2015-02-05 01:43:13 +08:00
def refresh_group_discussion_topics
if self . group_category
self . group_category . discussion_topics . active . each ( & :update_subtopics )
end
end
2015-03-11 23:48:14 +08:00
def includes_user? ( user , membership_scope = group_memberships )
return false if user . nil? || user . new_record?
membership_scope . where ( user_id : user ) . exists?
end
2012-03-03 08:05:06 +08:00
alias_method :participating_users_association , :participating_users
def participating_users ( user_ids = nil )
user_ids ?
2013-03-19 03:07:47 +08:00
participating_users_association . where ( :id = > user_ids ) :
2012-03-03 08:05:06 +08:00
participating_users_association
end
2014-11-27 04:18:15 +08:00
def all_real_students
2015-01-13 06:39:33 +08:00
return self . context . all_real_students . where ( " users.id IN (?) " , self . users . pluck ( :id ) ) if self . context . respond_to? " all_real_students "
self . users
2014-11-27 04:18:15 +08:00
end
2012-07-24 05:29:18 +08:00
def wiki_with_create
Wiki . wiki_for_context ( self )
2011-02-01 09:57:29 +08:00
end
2012-07-24 05:29:18 +08:00
alias_method_chain :wiki , :create
2012-01-04 04:30:49 +08:00
2012-08-30 04:57:00 +08:00
def auto_accept?
2014-04-23 01:37:54 +08:00
self . group_category &&
2012-05-16 05:50:43 +08:00
self . group_category . allows_multiple_memberships? &&
self . join_level == 'parent_context_auto_join'
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-08-30 04:57:00 +08:00
def allow_join_request?
2014-04-23 01:37:54 +08:00
self . group_category &&
2012-05-16 05:50:43 +08:00
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
2012-01-04 04:30:49 +08:00
2011-10-08 07:19:20 +08:00
def allow_self_signup? ( user )
2012-05-16 05:50:43 +08:00
self . group_category &&
( self . group_category . unrestricted_self_signup? ||
( self . group_category . restricted_self_signup? && self . has_common_section_with_user? ( user ) ) )
end
2013-05-02 23:18:22 +08:00
def full?
2014-04-23 01:37:54 +08:00
! student_organized? && ( ( ! max_membership && group_category_limit_met? ) || ( max_membership && participating_users . size > = max_membership ) )
end
def group_category_limit_met?
2013-05-02 23:18:22 +08:00
group_category && group_category . group_limit && participating_users . size > = group_category . group_limit
end
2014-05-20 02:18:17 +08:00
private :group_category_limit_met?
2013-05-02 23:18:22 +08:00
2014-04-23 01:37:54 +08:00
def student_organized?
group_category && group_category . student_organized?
end
def update_max_membership_from_group_category
2014-12-11 09:15:54 +08:00
if ( ! max_membership || max_membership == 0 ) && group_category && group_category . group_limit
2014-04-23 01:37:54 +08:00
self . max_membership = group_category . group_limit
end
end
2012-08-30 04:57:00 +08:00
def free_association? ( user )
auto_accept? || allow_join_request? || allow_self_signup? ( user )
2011-10-08 07:19:20 +08:00
end
2012-01-04 04:30:49 +08:00
2012-08-31 05:15:34 +08:00
def allow_student_forum_attachments
context . respond_to? ( :allow_student_forum_attachments ) && context . allow_student_forum_attachments
end
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
2013-03-08 08:08:47 +08:00
raise " DONT USE THIS, use .short_name instead " unless Rails . env . production?
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2015-03-10 03:23:14 +08:00
def context_available?
return false unless self . context
case self . context
when Course
self . context . available?
else
true
end
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 )
2014-09-12 03:44:34 +08:00
self . group_memberships . where ( user_id : user ) . first if user
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-05-16 05:50:43 +08:00
def has_member? ( user )
2012-06-21 05:05:53 +08:00
return nil unless user . present?
2014-02-28 07:36:27 +08:00
if self . group_memberships . loaded?
return self . group_memberships . to_a . find { | gm | gm . accepted? && gm . user_id == user . id }
else
2014-09-12 03:44:34 +08:00
self . participating_group_memberships . where ( user_id : user ) . first
2014-02-28 07:36:27 +08:00
end
2012-05-16 05:50:43 +08:00
end
def has_moderator? ( user )
2012-06-21 05:05:53 +08:00
return nil unless user . present?
2014-02-28 07:36:27 +08:00
if self . group_memberships . loaded?
return self . group_memberships . to_a . find { | gm | gm . accepted? && gm . user_id == user . id && gm . moderator }
end
2014-09-12 03:44:34 +08:00
self . participating_group_memberships . moderators . where ( user_id : user ) . first
2012-05-16 05:50:43 +08:00
end
2013-11-08 02:01:49 +08:00
def should_add_creator? ( creator )
self . group_category &&
( self . group_category . communities? || ( self . group_category . student_organized? && self . context . user_is_student? ( creator ) ) )
2012-05-30 06:55:40 +08:00
end
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
2013-11-21 08:15:12 +08:00
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
2013-01-09 06:06:48 +08:00
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
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'
2013-10-30 05:34:26 +08:00
self . deleted_at = Time . now . utc
2011-02-01 09:57:29 +08:00
self . save
end
2012-01-04 04:30:49 +08:00
2013-07-11 05:39:38 +08:00
Bookmarker = BookmarkedCollection :: SimpleBookmarker . new ( Group , :name , :id )
2014-07-02 03:38:26 +08:00
scope :active , - > { where ( " groups.workflow_state<>'deleted' " ) }
scope :by_name , - > { order ( Bookmarker . order_by ) }
scope :uncategorized , - > { where ( " groups.group_category_id IS NULL " ) }
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
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
2012-06-02 03:18:32 +08:00
# 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
2012-06-02 03:18:32 +08:00
attrs = { :user = > user , :moderator = > ! ! moderator }
2012-05-16 05:50:43 +08:00
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
2014-09-12 03:44:34 +08:00
if member = self . group_memberships . where ( user_id : user ) . first
2012-06-02 03:18:32 +08:00
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
2012-09-27 03:05:39 +08:00
# permissions for this user in the group are probably different now
2014-05-03 00:35:29 +08:00
clear_permissions_cache ( user )
2011-02-01 09:57:29 +08:00
return member
end
2012-01-04 04:30:49 +08:00
2014-12-04 08:16:40 +08:00
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
2013-12-06 03:38:42 +08:00
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 )
2014-05-03 00:35:29 +08:00
users . each { | user | clear_permissions_cache ( user ) }
2013-12-06 03:38:42 +08:00
2015-03-10 03:23:14 +08:00
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
2013-12-06 03:38:42 +08:00
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 |
2014-07-11 01:22:01 +08:00
options . merge ( { :user_id = > user . id , :uuid = > CanvasSlug . generate_securish_uuid } )
2013-12-06 03:38:42 +08:00
} )
end
2011-02-01 09:57:29 +08:00
def invite_user ( user )
2012-05-16 05:50:43 +08:00
self . add_user ( user , 'invited' )
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 request_user ( user )
2012-05-16 05:50:43 +08:00
self . add_user ( user , 'requested' )
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 invitees = ( params )
invitees = [ ]
( params || { } ) . each do | key , val |
if self . context
2014-09-12 03:44:34 +08:00
invitees << self . context . users . where ( id : key . to_i ) . first if val != '0'
2011-02-01 09:57:29 +08:00
else
2014-09-12 03:44:34 +08:00
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
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def peer_groups
2012-05-16 05:50:43 +08:00
return [ ] if ! self . context || ! self . group_category || self . group_category . allows_multiple_memberships?
2013-03-19 03:07:47 +08:00
self . group_category . groups . where ( " id<>? " , self ) . all
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
2015-04-28 01:47:25 +08:00
2011-02-01 09:57:29 +08:00
def log_merge_result ( text )
@merge_results || = [ ]
@merge_results << text
end
2015-04-28 01:47:25 +08:00
2011-02-01 09:57:29 +08:00
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
2014-07-11 01:22:01 +08:00
self . name || = CanvasSlug . generate_securish_uuid
self . uuid || = CanvasSlug . 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'
2012-05-16 05:50:43 +08:00
self . is_public || = false
2012-05-30 06:55:40 +08:00
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 )
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
2015-04-28 01:47:25 +08:00
2011-06-03 23:54:00 +08:00
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
2015-05-28 07:42:25 +08:00
# 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
2012-08-30 04:57:00 +08:00
end
2013-06-06 09:21:29 +08:00
def users_visible_to ( user )
2014-05-03 00:35:29 +08:00
grants_right? ( user , :read ) ? users : users . none
2013-06-06 09:21:29 +08:00
end
2012-08-30 04:57:00 +08:00
# Helper needed by several permissions, use grants_right?(user, :participate)
def can_participate? ( user )
2014-02-28 07:36:27 +08:00
return true if can_participate
2012-08-30 04:57:00 +08:00
return false unless user . present? && self . context . present?
return true if self . group_category . try ( :communities? )
if self . context . is_a? ( Course )
2014-02-28 07:36:27 +08:00
return self . context . enrollments . not_fake . except ( :includes ) . where ( :user_id = > user . id ) . exists?
2012-08-30 04:57:00 +08:00
elsif self . context . is_a? ( Account )
2014-02-28 07:36:27 +08:00
return self . context . user_account_associations . where ( :user_id = > user . id ) . exists?
2012-08-30 04:57:00 +08:00
end
return false
2011-02-01 09:57:29 +08:00
end
2012-08-30 04:57:00 +08:00
private :can_participate?
2011-03-02 01:44:41 +08:00
2013-02-08 04:09:05 +08:00
def user_can_manage_own_discussion_posts? ( user )
2015-02-19 06:21:34 +08:00
return true unless self . context . is_a? ( Course )
context . user_can_manage_own_discussion_posts? ( user )
2013-02-08 04:09:05 +08:00
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
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-05-30 06:55:40 +08:00
def members_count
self . participating_group_memberships . count
2012-02-16 05:40:13 +08:00
end
2011-10-21 03:06:55 +08:00
def quota
2013-06-05 02:50:15 +08:00
return self . storage_quota || self . account . default_group_storage_quota || self . class . default_storage_quota
end
def self . default_storage_quota
2013-10-05 04:02:49 +08:00
Setting . get ( 'group_default_quota' , 50 . megabytes . to_s ) . to_i
2011-10-21 03:06:55 +08:00
end
2013-03-22 07:03:24 +08:00
def storage_quota_mb
quota / 1 . megabyte
end
2014-04-23 01:37:54 +08:00
2013-03-22 07:03:24 +08:00
def storage_quota_mb = ( val )
self . storage_quota = val . try ( :to_i ) . try ( :megabytes )
end
2014-04-23 01:37:54 +08:00
2013-12-18 05:21:40 +08:00
TAB_HOME , TAB_PAGES , TAB_PEOPLE , TAB_DISCUSSIONS , TAB_FILES ,
2012-07-20 01:13:21 +08:00
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 = [
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 } ,
2014-10-10 10:18:35 +08:00
{ :id = > TAB_PAGES , :label = > t ( " # group.tabs.pages " , " Pages " ) , :css_class = > 'pages' , :href = > :group_wiki_path } ,
2012-10-19 10:13:51 +08:00
{ :id = > TAB_PEOPLE , :label = > t ( " # group.tabs.people " , " People " ) , :css_class = > 'people' , :href = > :group_users_path } ,
2011-09-09 06:11:46 +08:00
{ :id = > TAB_DISCUSSIONS , :label = > t ( " # group.tabs.discussions " , " Discussions " ) , :css_class = > 'discussions' , :href = > :group_discussion_topics_path } ,
2012-06-06 05:06:36 +08:00
{ :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-06-01 06:47:04 +08:00
2012-06-28 07:22:41 +08:00
if root_account . try :canvas_network_enabled?
2012-06-14 06:19:23 +08:00
available_tabs << { :id = > TAB_PROFILE , :label = > t ( '#tabs.profile' , 'Profile' ) , :css_class = > 'profile' , :href = > :group_profile_path }
end
2014-05-03 00:35: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 , :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 )
2014-04-23 01:37:54 +08:00
available_tabs << { :id = > TAB_SETTINGS , :label = > t ( '#tabs.settings' , 'Settings' ) , :css_class = > 'settings' , :href = > :edit_group_path }
2012-08-06 23:46:18 +08:00
end
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-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
2012-06-01 03:21:22 +08:00
def self . join_levels
[
[ " invitation_only " , " Invite only " ] ,
[ " parent_context_auto_join " , " Auto join " ] ,
[ " parent_context_request " , " Request to join " ]
]
end
2012-06-07 05:14:35 +08:00
2012-11-17 06:00:05 +08:00
def associated_shards
[ Shard . default ]
end
2013-07-10 00:14:52 +08:00
2013-11-15 02:22:17 +08:00
# Public: Determine whether a feature is enabled, deferring to the group's context.
2013-07-10 00:14:52 +08:00
#
# Returns a boolean.
2013-11-15 02:22:17 +08:00
def feature_enabled? ( feature )
2013-08-22 06:09:35 +08:00
# shouldn't matter, but most specs create anonymous (contextless) groups :(
return false if context . nil?
2013-11-15 02:22:17 +08:00
context . feature_enabled? ( feature )
2013-07-10 00:14:52 +08:00
end
2013-09-18 06:24:57 +08:00
def serialize_permissions ( permissions_hash , user , session )
permissions_hash . merge (
2015-03-07 08:33:59 +08:00
create_discussion_topic : DiscussionTopic . context_allows_user_to_create? ( self , user , session ) ,
create_announcement : Announcement . context_allows_user_to_create? ( self , user , session )
2013-09-18 06:24:57 +08:00
)
end
2014-05-22 22:37:18 +08:00
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
2014-09-11 00:06:39 +08:00
def account_chain
@account_chain || = Account . account_chain ( account_id )
2014-12-10 00:55:09 +08:00
@account_chain . dup
2014-09-11 00:06:39 +08:00
end
2014-11-06 04:14:57 +08:00
def sortable_name
name
end
2011-02-01 09:57:29 +08:00
end