828 lines
32 KiB
Ruby
828 lines
32 KiB
Ruby
#
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
require 'atom'
|
|
|
|
# @API Groups
|
|
#
|
|
# Groups serve as the data for a few different ideas in Canvas. The first is
|
|
# that they can be a community in the canvas network. The second is that they
|
|
# can be organized by students in a course, for study or communication (but not
|
|
# grading). The third is that they can be organized by teachers or account
|
|
# administrators for the purpose of projects, assignments, and grading. This
|
|
# last kind of group is always part of a group category, which adds the
|
|
# restriction that a user may only be a member of one group per category.
|
|
#
|
|
# All of these types of groups function similarly, and can be the parent
|
|
# context for many other types of functionality and interaction, such as
|
|
# collections, discussions, wikis, and shared files.
|
|
#
|
|
# @model Group
|
|
# {
|
|
# "id": "Group",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The ID of the group.",
|
|
# "example": 17,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "The display name of the group.",
|
|
# "example": "Math Group 1",
|
|
# "type": "string"
|
|
# },
|
|
# "description": {
|
|
# "description": "A description of the group. This is plain text.",
|
|
# "type": "string"
|
|
# },
|
|
# "is_public": {
|
|
# "description": "Whether or not the group is public. Currently only community groups can be made public. Also, once a group has been set to public, it cannot be changed back to private.",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "followed_by_user": {
|
|
# "description": "Whether or not the current user is following this group.",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "join_level": {
|
|
# "description": "How people are allowed to join the group. For all groups except for community groups, the user must share the group's parent course or account. For student organized or community groups, where a user can be a member of as many or few as they want, the applicable levels are 'parent_context_auto_join', 'parent_context_request', and 'invitation_only'. For class groups, where students are divided up and should only be part of one group of the category, this value will always be 'invitation_only', and is not relevant. * If 'parent_context_auto_join', anyone can join and will be automatically accepted. * If 'parent_context_request', anyone can request to join, which must be approved by a group moderator. * If 'invitation_only', only those how have received an invitation my join the group, by accepting that invitation.",
|
|
# "example": "invitation_only",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "parent_context_auto_join",
|
|
# "parent_context_request",
|
|
# "invitation_only"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "members_count": {
|
|
# "description": "The number of members currently in the group",
|
|
# "example": 0,
|
|
# "type": "integer"
|
|
# },
|
|
# "avatar_url": {
|
|
# "description": "The url of the group's avatar",
|
|
# "example": "https://<canvas>/files/avatar_image.png",
|
|
# "type": "string"
|
|
# },
|
|
# "context_type": {
|
|
# "description": "The course or account that the group belongs to. The pattern here is that whatever the context_type is, there will be an _id field named after that type. So if instead context_type was 'account', the course_id field would be replaced by an account_id field.",
|
|
# "example": "Course",
|
|
# "type": "string"
|
|
# },
|
|
# "course_id": {
|
|
# "example": 3,
|
|
# "type": "integer"
|
|
# },
|
|
# "role": {
|
|
# "description": "Certain types of groups have special role designations. Currently, these include: 'communities', 'student_organized', and 'imported'. Regular course/account groups have a role of null.",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "communities",
|
|
# "student_organized",
|
|
# "imported"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "group_category_id": {
|
|
# "description": "The ID of the group's category.",
|
|
# "example": 4,
|
|
# "type": "integer"
|
|
# },
|
|
# "sis_group_id": {
|
|
# "description": "The SIS ID of the group. Only included if the user has permission to view SIS information.",
|
|
# "example": "group4a",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_import_id": {
|
|
# "description": "The id of the SIS import if created through SIS. Only included if the user has permission to manage SIS information.",
|
|
# "example": 14,
|
|
# "type": "integer"
|
|
# },
|
|
# "storage_quota_mb": {
|
|
# "description": "the storage quota for the group, in megabytes",
|
|
# "example": 50,
|
|
# "type": "integer"
|
|
# },
|
|
# "permissions": {
|
|
# "description": "optional: the permissions the user has for the group. returned only for a single group and include[]=permissions",
|
|
# "example": {"create_discussion_topic": true, "create_announcement": true},
|
|
# "type": "object",
|
|
# "key": { "type": "string" },
|
|
# "value": { "type": "boolean" }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class GroupsController < ApplicationController
|
|
before_action :get_context
|
|
before_action :require_user, :only => %w[index accept_invitation activity_stream activity_stream_summary]
|
|
|
|
include Api::V1::Attachment
|
|
include Api::V1::Group
|
|
include Api::V1::GroupCategory
|
|
include Context
|
|
|
|
SETTABLE_GROUP_ATTRIBUTES = %w(
|
|
name description join_level is_public group_category avatar_attachment
|
|
storage_quota_mb max_membership leader
|
|
).freeze
|
|
|
|
include TextHelper
|
|
|
|
def context_group_members
|
|
@group = @context
|
|
if authorized_action(@group, @current_user, :read_roster)
|
|
render :json => @group.members_json_cached
|
|
end
|
|
end
|
|
|
|
def unassigned_members
|
|
category = @context.group_categories.where(id: params[:category_id]).first
|
|
return render :json => {}, :status => :not_found unless category
|
|
page = (params[:page] || 1).to_i rescue 1
|
|
per_page = Api.per_page_for(self, default: 15, max: 100)
|
|
if category && !category.student_organized?
|
|
groups = category.groups.active
|
|
else
|
|
groups = []
|
|
end
|
|
|
|
users = @context.users_not_in_groups(groups, order: User.sortable_name_order_by_clause('users')).
|
|
paginate(page: page, per_page: per_page)
|
|
|
|
if authorized_action(@context, @current_user, :manage)
|
|
json = {
|
|
:pages => users.total_pages,
|
|
:current_page => users.current_page,
|
|
:next_page => users.next_page,
|
|
:previous_page => users.previous_page,
|
|
:total_entries => users.total_entries,
|
|
:users => users.map { |u| u.group_member_json(@context) }
|
|
}
|
|
json[:pagination_html] = render_to_string(:partial => 'user_pagination', :locals => { :users => users }) unless params[:no_html]
|
|
render :json => json
|
|
end
|
|
end
|
|
|
|
# @API List your groups
|
|
#
|
|
# Returns a paginated list of active groups for the current user.
|
|
#
|
|
# @argument context_type [String, "Account"|"Course"]
|
|
# Only include groups that are in this type of context.
|
|
#
|
|
# @argument include[] [String, "tabs"]
|
|
# - "tabs": Include the list of tabs configured for each group. See the
|
|
# {api:TabsController#index List available tabs API} for more information.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/self/groups?context_type=Account \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [Group]
|
|
def index
|
|
return context_index if @context
|
|
includes = {:include => params[:include]}
|
|
groups_scope = @current_user.current_groups
|
|
respond_to do |format|
|
|
format.html do
|
|
groups_scope = groups_scope.by_name
|
|
groups_scope = groups_scope.where(:context_type => params[:context_type]) if params[:context_type]
|
|
groups_scope = groups_scope.preload(:group_category, :context)
|
|
|
|
groups = groups_scope.shard(@current_user).to_a
|
|
groups.select!{|group| group.context_type != 'Course' || group.context.grants_right?(@current_user, :read)}
|
|
|
|
# Split the groups out into those in concluded courses and those not in concluded courses
|
|
@current_groups, @previous_groups = groups.partition do |group|
|
|
group.context_type != 'Course' || !group.context.concluded?('StudentEnrollment')
|
|
end
|
|
end
|
|
|
|
format.json do
|
|
@groups = ShardedBookmarkedCollection.build(Group::Bookmarker, groups_scope) do |scope|
|
|
scope = scope.where(:context_type => params[:context_type]) if params[:context_type]
|
|
scope.preload(:group_category, :context)
|
|
end
|
|
@groups = Api.paginate(@groups, self, api_v1_current_user_groups_url)
|
|
render :json => (@groups.map { |g| group_json(g, @current_user, session,includes) })
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API List the groups available in a context.
|
|
#
|
|
# Returns the paginated list of active groups in the given context that are visible to user.
|
|
#
|
|
# @argument only_own_groups [Boolean]
|
|
# Will only include groups that the user belongs to if this is set
|
|
#
|
|
# @argument include[] [String, "tabs"]
|
|
# - "tabs": Include the list of tabs configured for each group. See the
|
|
# {api:TabsController#index List available tabs API} for more information.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/1/groups \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [Group]
|
|
def context_index
|
|
return unless authorized_action(@context, @current_user, :read_roster)
|
|
@groups = all_groups = @context.groups.active
|
|
.order(GroupCategory::Bookmarker.order_by, Group::Bookmarker.order_by)
|
|
.eager_load(:group_category)
|
|
|
|
unless api_request?
|
|
if @context.is_a?(Account)
|
|
user_crumb = t('#crumbs.users', "Users")
|
|
@active_tab = "users"
|
|
@group_user_type = "user"
|
|
@allow_self_signup = false
|
|
else
|
|
user_crumb = t('#crumbs.people', "People")
|
|
@active_tab = "people"
|
|
@group_user_type = "student"
|
|
@allow_self_signup = true
|
|
if @context.grants_right? @current_user, session, :read_as_admin
|
|
js_env STUDENT_CONTEXT_CARDS_ENABLED: @domain_root_account.feature_enabled?(:student_context_cards)
|
|
end
|
|
end
|
|
|
|
add_crumb user_crumb, named_context_url(@context, :context_users_url)
|
|
add_crumb t('#crumbs.groups', "Groups"), named_context_url(@context, :context_groups_url)
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
@categories = @context.group_categories.order("role <> 'student_organized'", GroupCategory.best_unicode_collation_key('name'))
|
|
@user_groups = @current_user.group_memberships_for(@context) if @current_user
|
|
|
|
if @context.grants_right?(@current_user, session, :manage_groups)
|
|
categories_json = @categories.map{ |cat| group_category_json(cat, @current_user, session, include: ["progress_url", "unassigned_users_count", "groups_count"]) }
|
|
uncategorized = @context.groups.active.uncategorized.to_a
|
|
if uncategorized.present?
|
|
json = group_category_json(GroupCategory.uncategorized, @current_user, session)
|
|
json["groups"] = uncategorized.map{ |group| group_json(group, @current_user, session) }
|
|
categories_json << json
|
|
end
|
|
|
|
js_env group_categories: categories_json,
|
|
group_user_type: @group_user_type,
|
|
allow_self_signup: @allow_self_signup
|
|
if @context.is_a?(Course)
|
|
# get number of sections with students in them so we can enforce a min group size for random assignment on sections
|
|
js_env(:student_section_count => @context.enrollments.active_or_pending.where(:type => "StudentEnrollment").distinct.count(:course_section_id))
|
|
end
|
|
# since there are generally lots of users in an account, always do large roster view
|
|
@js_env[:IS_LARGE_ROSTER] ||= @context.is_a?(Account)
|
|
render :context_manage_groups
|
|
else
|
|
return render_unauthorized_action if @context.is_a?(Account)
|
|
@groups = @user_groups = @groups & (@user_groups || [])
|
|
@available_groups = (all_groups - @user_groups).select do |group|
|
|
group.grants_right?(@current_user, :join)
|
|
end
|
|
render :context_groups
|
|
end
|
|
end
|
|
|
|
format.atom { render :xml => @groups.map { |group| group.to_atom }.to_xml }
|
|
|
|
format.json do
|
|
path = send("api_v1_#{@context.class.to_s.downcase}_user_groups_url")
|
|
|
|
if value_to_boolean(params[:only_own_groups])
|
|
all_groups = all_groups.merge(@current_user.current_groups)
|
|
end
|
|
@paginated_groups = Api.paginate(all_groups, self, path)
|
|
render :json => @paginated_groups.map { |g|
|
|
include_inactive_users = value_to_boolean(params[:include_inactive_users])
|
|
group_json(g, @current_user, session, :include => Array(params[:include]),
|
|
:include_inactive_users => include_inactive_users)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Get a single group
|
|
#
|
|
# Returns the data for a single group, or a 401 if the caller doesn't have
|
|
# the rights to see it.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/<group_id> \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @argument include[] [String, "permissions", "tabs"]
|
|
# - "permissions": Include permissions the current user has
|
|
# for the group.
|
|
# - "tabs": Include the list of tabs configured for each group. See the
|
|
# {api:TabsController#index List available tabs API} for more information.
|
|
#
|
|
# @returns Group
|
|
def show
|
|
find_group
|
|
respond_to do |format|
|
|
format.html do
|
|
if @group && @group.context
|
|
add_crumb @group.context.short_name, named_context_url(@group.context, :context_url)
|
|
add_crumb @group.short_name, named_context_url(@group, :context_url)
|
|
elsif @group
|
|
add_crumb @group.short_name, named_context_url(@group, :context_url)
|
|
end
|
|
@context = @group
|
|
if @group.deleted? && @group.context
|
|
flash[:notice] = t('notices.already_deleted', "That group has been deleted")
|
|
redirect_to named_context_url(@group.context, :context_url)
|
|
return
|
|
end
|
|
@current_conferences = @group.web_conferences.active.select{|c| c.active? && c.users.include?(@current_user) } rescue []
|
|
@scheduled_conferences = @context.web_conferences.active.select{|c| c.scheduled? && c.users.include?(@current_user)} rescue []
|
|
@stream_items = @current_user.try(:cached_recent_stream_items, { :contexts => @context }) || []
|
|
if params[:join] && @group.grants_right?(@current_user, :join)
|
|
if @group.full?
|
|
flash[:error] = t('errors.group_full', 'The group is full.')
|
|
redirect_to course_groups_url(@group.context)
|
|
return
|
|
end
|
|
@group.request_user(@current_user)
|
|
if !@group.grants_right?(@current_user, session, :read)
|
|
render :membership_pending
|
|
return
|
|
else
|
|
flash[:notice] = t('notices.welcome', "Welcome to the group %{group_name}!", :group_name => @group.name)
|
|
redirect_to named_context_url(@group.context, :context_groups_url)
|
|
return
|
|
end
|
|
end
|
|
if params[:leave] && @group.grants_right?(@current_user, :leave)
|
|
membership = @group.membership_for_user(@current_user)
|
|
if membership
|
|
membership.destroy
|
|
flash[:notice] = t('notices.goodbye', "You have removed yourself from the group %{group_name}.", :group_name => @group.name)
|
|
redirect_to named_context_url(@group.context, :context_groups_url)
|
|
return
|
|
end
|
|
end
|
|
if authorized_action(@group, @current_user, :read)
|
|
set_badge_counts_for(@group, @current_user)
|
|
@home_page = @group.wiki.front_page
|
|
end
|
|
end
|
|
format.json do
|
|
if authorized_action(@group, @current_user, :read)
|
|
render :json => group_json(@group, @current_user, session, :include => Array(params[:include]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def new
|
|
if authorized_action(@context, @current_user, :manage_groups)
|
|
@group = @context.groups.build
|
|
end
|
|
end
|
|
|
|
# @API Create a group
|
|
#
|
|
# Creates a new group. Groups created using the "/api/v1/groups/"
|
|
# endpoint will be community groups.
|
|
#
|
|
# @argument name [String]
|
|
# The name of the group
|
|
#
|
|
# @argument description [String]
|
|
# A description of the group
|
|
#
|
|
# @argument is_public [Boolean]
|
|
# whether the group is public (applies only to community groups)
|
|
#
|
|
# @argument join_level [String, "parent_context_auto_join"|"parent_context_request"|"invitation_only"]
|
|
#
|
|
# @argument storage_quota_mb [Integer]
|
|
# The allowed file storage for the group, in megabytes. This parameter is
|
|
# ignored if the caller does not have the manage_storage_quotas permission.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups \
|
|
# -F 'name=Math Teachers' \
|
|
# -F 'description=A place to gather resources for our classes.' \
|
|
# -F 'is_public=true' \
|
|
# -F 'join_level=parent_context_auto_join' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns Group
|
|
def create
|
|
attrs = api_request? ? params : params.require(:group)
|
|
attrs = attrs.permit(:name, :description, :join_level, :is_public, :storage_quota_mb, :max_membership)
|
|
|
|
if api_request?
|
|
if params[:group_category_id]
|
|
group_category = GroupCategory.active.find(params[:group_category_id])
|
|
return render :json => {}, :status => bad_request unless group_category
|
|
@context = group_category.context
|
|
attrs[:group_category] = group_category
|
|
return unless authorized_action(group_category.context, @current_user, :manage_groups)
|
|
else
|
|
@context = @domain_root_account
|
|
attrs[:group_category] = GroupCategory.communities_for(@context)
|
|
end
|
|
elsif params[:group]
|
|
group_category_id = params[:group].delete :group_category_id
|
|
if group_category_id && @context.grants_right?(@current_user, session, :manage_groups)
|
|
group_category = @context.group_categories.where(id: group_category_id).first
|
|
return render :json => {}, :status => :bad_request unless group_category
|
|
attrs[:group_category] = group_category
|
|
else
|
|
attrs[:group_category] = nil
|
|
end
|
|
end
|
|
|
|
attrs.delete :storage_quota_mb unless @context.grants_right? @current_user, session, :manage_storage_quotas
|
|
@group = @context.groups.temp_record(attrs.slice(*SETTABLE_GROUP_ATTRIBUTES))
|
|
|
|
if authorized_action(@group, @current_user, :create)
|
|
respond_to do |format|
|
|
if @group.save
|
|
@group.add_user(@current_user, 'accepted', true) if @group.should_add_creator?(@current_user)
|
|
@group.invitees = params[:invitees]
|
|
flash[:notice] = t('notices.create_success', 'Group was successfully created.')
|
|
format.html { redirect_to group_url(@group) }
|
|
format.json { render :json => group_json(@group, @current_user, session, {include: ['users', 'group_category', 'permissions']}) }
|
|
else
|
|
format.html { render :new }
|
|
format.json { render :json => @group.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Edit a group
|
|
#
|
|
# Modifies an existing group. Note that to set an avatar image for the
|
|
# group, you must first upload the image file to the group, and the use the
|
|
# id in the response as the argument to this function. See the
|
|
# {file:file_uploads.html File Upload Documentation} for details on the file
|
|
# upload workflow.
|
|
#
|
|
# @argument name [String]
|
|
# The name of the group
|
|
#
|
|
# @argument description [String]
|
|
# A description of the group
|
|
#
|
|
# @argument is_public [Boolean]
|
|
# Whether the group is public (applies only to community groups). Currently
|
|
# you cannot set a group back to private once it has been made public.
|
|
#
|
|
# @argument join_level [String, "parent_context_auto_join"|"parent_context_request"|"invitation_only"]
|
|
#
|
|
# @argument avatar_id [Integer]
|
|
# The id of the attachment previously uploaded to the group that you would
|
|
# like to use as the avatar image for this group.
|
|
#
|
|
# @argument storage_quota_mb [Integer]
|
|
# The allowed file storage for the group, in megabytes. This parameter is
|
|
# ignored if the caller does not have the manage_storage_quotas permission.
|
|
#
|
|
# @argument members[] [String]
|
|
# An array of user ids for users you would like in the group.
|
|
# Users not in the group will be sent invitations. Existing group
|
|
# members who aren't in the list will be removed from the group.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/<group_id> \
|
|
# -X PUT \
|
|
# -F 'name=Algebra Teachers' \
|
|
# -F 'join_level=parent_context_request' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns Group
|
|
def update
|
|
find_group
|
|
group_params = api_request? ? params : params.require(:group)
|
|
attrs = group_params.permit(:name, :description, :join_level, :is_public, :avatar_id, :storage_quota_mb, :max_membership,
|
|
:leader => strong_anything, :members => strong_anything)
|
|
attrs[:leader] = nil if group_params.has_key?(:leader) && !group_params[:leader].present?
|
|
|
|
if !api_request? && params[:group][:group_category_id]
|
|
group_category_id = params[:group].delete :group_category_id
|
|
group_category = @context.group_categories.where(id: group_category_id).first
|
|
return render :json => {}, :status => :bad_request unless group_category
|
|
attrs[:group_category] = group_category
|
|
end
|
|
|
|
attrs.delete :storage_quota_mb unless @group.context.grants_right? @current_user, session, :manage_storage_quotas
|
|
|
|
attrs[:avatar_attachment] = @group.active_images.where(id: attrs[:avatar_id]).first if attrs[:avatar_id]
|
|
|
|
if attrs[:leader]
|
|
membership = @group.group_memberships.where(user_id: attrs[:leader][:id]).first
|
|
return render :json => {}, :status => :bad_request unless membership
|
|
attrs[:leader] = membership.user
|
|
end
|
|
|
|
if authorized_action(@group, @current_user, :update)
|
|
respond_to do |format|
|
|
@group.transaction do
|
|
@group.update_attributes(attrs.slice(*SETTABLE_GROUP_ATTRIBUTES))
|
|
if attrs[:members]
|
|
user_ids = Api.value_to_array(attrs[:members]).map(&:to_i).uniq
|
|
if @group.context
|
|
users = @group.context.users.where(id: user_ids)
|
|
else
|
|
users = User.where(id: user_ids)
|
|
end
|
|
@memberships = @group.set_users(users)
|
|
end
|
|
end
|
|
|
|
if !@group.errors.any?
|
|
@group.users.touch_all
|
|
flash[:notice] = t('notices.update_success', 'Group was successfully updated.')
|
|
format.html { redirect_to clean_return_to(params[:return_to]) || group_url(@group) }
|
|
format.json { render :json => group_json(@group, @current_user, session, {include: ['users', 'group_category', 'permissions']}) }
|
|
else
|
|
format.html { render :edit }
|
|
format.json { render :json => @group.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Delete a group
|
|
#
|
|
# Deletes a group and removes all members.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/<group_id> \
|
|
# -X DELETE \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns Group
|
|
def destroy
|
|
find_group
|
|
if authorized_action(@group, @current_user, :delete)
|
|
if @group.destroy
|
|
flash[:notice] = t('notices.delete_success', "Group successfully deleted")
|
|
respond_to do |format|
|
|
format.html { redirect_to(dashboard_url) }
|
|
format.json { render :json => group_json(@group, @current_user, session) }
|
|
end
|
|
else
|
|
respond_to do |format|
|
|
format.html { redirect_to(dashboard_url) }
|
|
format.json { render :json => @group.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Invite others to a group
|
|
#
|
|
# @subtopic Group Memberships
|
|
#
|
|
# Sends an invitation to all supplied email addresses which will allow the
|
|
# receivers to join the group.
|
|
#
|
|
# @argument invitees[] [Required, String]
|
|
# An array of email addresses to be sent invitations.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/<group_id>/invite \
|
|
# -F 'invitees[]=leonard@example.com' \
|
|
# -F 'invitees[]=sheldon@example.com' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
def invite
|
|
find_group
|
|
if authorized_action(@group, @current_user, :manage)
|
|
root_account = @group.context.try(:root_account) || @domain_root_account
|
|
ul = UserList.new(params[:invitees],
|
|
root_account: root_account,
|
|
search_method: :preferred,
|
|
current_user: @current_user)
|
|
@memberships = []
|
|
ul.users.each{ |u| @memberships << @group.invite_user(u) }
|
|
render :json => @memberships.map{ |gm| group_membership_json(gm, @current_user, session) }
|
|
end
|
|
end
|
|
|
|
def accept_invitation
|
|
find_group
|
|
@membership = @group.group_memberships.where(:uuid => params[:uuid]).first if @group
|
|
@membership.accept! if @membership.try(:invited?)
|
|
if @membership.try(:active?)
|
|
flash[:notice] = t('notices.welcome', "Welcome to the group %{group_name}!", :group_name => @group.name)
|
|
respond_to do |format|
|
|
format.html { redirect_to(group_url(@group)) }
|
|
format.json { render :json => group_membership_json(@membership, @current_user, session) }
|
|
end
|
|
else
|
|
flash[:notice] = t('notices.invalid_invitation', "", :group_name => @group.name)
|
|
respond_to do |format|
|
|
format.html { redirect_to(dashboard_url) }
|
|
format.json { render :json => "Unable to find associated group invitation", :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
|
|
def add_user
|
|
@group = @context
|
|
if authorized_action(@group, @current_user, :manage)
|
|
@membership = @group.add_user(User.find(params[:user_id]))
|
|
if @membership.valid?
|
|
@group.touch
|
|
render :json => @membership
|
|
else
|
|
render :json => @membership.errors, :status => :bad_request
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_user
|
|
@group = @context
|
|
if authorized_action(@group, @current_user, :manage)
|
|
@membership = @group.group_memberships.where(user_id: params[:user_id]).first
|
|
@membership.destroy
|
|
render :json => @membership
|
|
end
|
|
end
|
|
|
|
include Api::V1::User
|
|
# @API List group's users
|
|
#
|
|
# Returns a paginated list of users in the group.
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial name or full ID of the users to match and return in the
|
|
# results list. Must be at least 3 characters.
|
|
#
|
|
# @argument include[] [String, "avatar_url"]
|
|
# - "avatar_url": Include users' avatar_urls.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/1/users \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [User]
|
|
def users
|
|
return unless authorized_action(@context, @current_user, :read)
|
|
|
|
search_term = params[:search_term].presence
|
|
if search_term
|
|
users = UserSearch.for_user_in_context(search_term, @context, @current_user, session)
|
|
else
|
|
users = UserSearch.scope_for(@context, @current_user)
|
|
end
|
|
|
|
includes = Array(params[:include])
|
|
users = Api.paginate(users, self, api_v1_group_users_url)
|
|
json_users = users_json(users, @current_user, session, includes, @context, nil, Array(params[:exclude]))
|
|
|
|
if includes.include? 'group_submissions'
|
|
assignments = @context.group_category.assignments.active
|
|
submissions = Submission.active.where(assignment_id: assignments, workflow_state: 'submitted').select(:id, :user_id)
|
|
submissions_by_user = submissions.each_with_object({}) do |sub, obj|
|
|
obj[sub.user_id] = (obj[sub.user_id] || []).push(sub.id)
|
|
end
|
|
|
|
json_users.each do |user|
|
|
user[:group_submissions] = submissions_by_user[user[:id]]
|
|
end
|
|
end
|
|
|
|
if (includes.include? 'active_status') && (@context.context.is_a? Course)
|
|
user_ids = users.pluck('id')
|
|
enrollments = Enrollment.where(user_id: user_ids, course_id: @context.context_id)
|
|
|
|
inactive_students = enrollments.select(&:inactive?).pluck('user_id').to_set
|
|
json_users.each do |user|
|
|
user[:is_inactive] = inactive_students.include?(user[:id])
|
|
end
|
|
end
|
|
|
|
render :json => json_users
|
|
end
|
|
|
|
def public_feed
|
|
return unless get_feed_context(:only => [:group])
|
|
feed = Atom::Feed.new do |f|
|
|
f.title = t(:feed_title, "%{course_or_account_name} Feed", :course_or_account_name => @context.full_name)
|
|
f.links << Atom::Link.new(:href => group_url(@context), :rel => 'self')
|
|
f.updated = Time.now
|
|
f.id = group_url(@context)
|
|
end
|
|
@entries = []
|
|
@entries.concat @context.calendar_events.active
|
|
@entries.concat @context.discussion_topics.active
|
|
@entries.concat @context.wiki_pages
|
|
@entries = @entries.sort_by{|e| e.updated_at}
|
|
@entries.each do |entry|
|
|
feed.entries << entry.to_atom(:context => @context)
|
|
end
|
|
respond_to do |format|
|
|
format.atom { render :plain => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
# @API Upload a file
|
|
#
|
|
# Upload a file to the group.
|
|
#
|
|
# This API endpoint is the first step in uploading a file to a group.
|
|
# See the {file:file_uploads.html File Upload Documentation} for details on
|
|
# the file upload workflow.
|
|
#
|
|
# Only those with the "Manage Files" permission on a group can upload files
|
|
# to the group. By default, this is anybody participating in the
|
|
# group, or any admin over the group.
|
|
def create_file
|
|
@attachment = Attachment.new(:context => @context)
|
|
if authorized_action(@attachment, @current_user, :create)
|
|
api_attachment_preflight(@context, request, :check_quota => true)
|
|
end
|
|
end
|
|
|
|
include Api::V1::PreviewHtml
|
|
# @API Preview processed html
|
|
#
|
|
# Preview html content processed for this group
|
|
#
|
|
# @argument html [String]
|
|
# The html content to process
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/groups/<group_id>/preview_html \
|
|
# -F 'html=<p><badhtml></badhtml>processed html</p>' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "html": "<p>processed html</p>"
|
|
# }
|
|
def preview_html
|
|
get_context
|
|
if @context && authorized_action(@context, @current_user, :read)
|
|
render_preview_html
|
|
end
|
|
end
|
|
|
|
include Api::V1::StreamItem
|
|
# @API Group activity stream
|
|
# Returns the current user's group-specific activity stream, paginated.
|
|
#
|
|
# For full documentation, see the API documentation for the user activity
|
|
# stream, in the user api.
|
|
def activity_stream
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read)
|
|
api_render_stream(contexts: [@context], paginate_url: :api_v1_group_activity_stream_url)
|
|
end
|
|
end
|
|
|
|
# @API Group activity stream summary
|
|
# Returns a summary of the current user's group-specific activity stream.
|
|
#
|
|
# For full documentation, see the API documentation for the user activity
|
|
# stream summary, in the user api.
|
|
def activity_stream_summary
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read)
|
|
api_render_stream_summary([@context])
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def find_group
|
|
if api_request?
|
|
@group = api_find(Group.active, params[:group_id])
|
|
else
|
|
@group = @context if @context.is_a?(Group)
|
|
@group ||= api_find(@context ? @context.groups : Group, params[:id])
|
|
end
|
|
end
|
|
|
|
|
|
end
|