basic groups api; refs #8598
this adds show/create/edit/delete api actions for groups. currently create only allows creating community groups. the changes to these controller functions should be backwards compatible with course and student groups. anyone should be able to create a community (for now), but only moderators or account admins are allowed to edit/delete them. test plan: - test the four api actions from the command line - make sure managing course and student groups still works (there should be no visible changes here) Change-Id: I9cac73ab434b0ba464ecfe399ab42174eff9b48a Reviewed-on: https://gerrit.instructure.com/11148 Reviewed-by: Brian Palmer <brianp@instructure.com> Tested-by: Jenkins <jenkins@instructure.com>
This commit is contained in:
parent
3bf25a7013
commit
4c59a6efe7
|
@ -18,13 +18,68 @@
|
|||
|
||||
# @API Groups
|
||||
#
|
||||
# API for accessing group information.
|
||||
# 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.
|
||||
#
|
||||
# A Group object looks like:
|
||||
#
|
||||
# !!!javascript
|
||||
# {
|
||||
# // The ID of the group.
|
||||
# id: 17,
|
||||
#
|
||||
# // The display name of the group.
|
||||
# name: "Math Group 1",
|
||||
#
|
||||
# // A description of the group. This is plain text.
|
||||
# description: null,
|
||||
#
|
||||
# // 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.
|
||||
# is_public: false,
|
||||
#
|
||||
# // 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.
|
||||
# join_level: "invitation_only",
|
||||
#
|
||||
# // The number of members currently in the group
|
||||
# members_count: 0
|
||||
#
|
||||
# // The ID of the group's category.
|
||||
# group_category_id: 4,
|
||||
# }
|
||||
#
|
||||
class GroupsController < ApplicationController
|
||||
before_filter :get_context
|
||||
before_filter :require_context, :only => [:create_category, :delete_category]
|
||||
before_filter :get_group_as_context, :only => [:show]
|
||||
|
||||
include Api::V1::Attachment
|
||||
include Api::V1::Group
|
||||
|
||||
SETTABLE_GROUP_ATTRIBUTES = %w(name description join_level is_public group_category)
|
||||
|
||||
def context_group_members
|
||||
@group = @context
|
||||
|
@ -66,38 +121,101 @@ class GroupsController < ApplicationController
|
|||
@groups = @current_user ? @current_user.groups.active : []
|
||||
end
|
||||
|
||||
def show
|
||||
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
|
||||
def context_index
|
||||
add_crumb (@context.is_a?(Account) ? t('#crumbs.users', "Users") : t('#crumbs.people', "People")), named_context_url(@context, :context_users_url)
|
||||
add_crumb t('#crumbs.groups', "Groups"), named_context_url(@context, :context_groups_url)
|
||||
@active_tab = @context.is_a?(Account) ? "users" : "people"
|
||||
@groups = @context.groups.active
|
||||
@categories = @context.group_categories
|
||||
group_ids = @groups.map(&:id)
|
||||
|
||||
@user_groups = @current_user.group_memberships_for(@context).select{|g| group_ids.include?(g.id) } if @current_user
|
||||
@user_groups ||= []
|
||||
|
||||
@available_groups = (@groups - @user_groups).select{|g| g.can_join?(@current_user) }
|
||||
if !@context.grants_right?(@current_user, session, :manage_groups)
|
||||
@groups = @user_groups
|
||||
end
|
||||
@current_conferences = @group.web_conferences.select{|c| c.active? && c.users.include?(@current_user) } rescue []
|
||||
@groups = @current_user.group_memberships_for(@group.context) if @current_user
|
||||
if params[:join] && @group.can_join?(@current_user)
|
||||
@group.request_user(@current_user)
|
||||
if !@group.grants_right?(@current_user, session, :read)
|
||||
render :action => '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.can_leave?(@current_user)
|
||||
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)
|
||||
@home_page = WikiNamespace.default_for_context(@group).wiki.wiki_page
|
||||
# sort by name, but with the student organized category in the back
|
||||
@categories = @categories.sort_by{|c| [ (c.student_organized? ? 1 : 0), c.name ] }
|
||||
@groups = @groups.sort_by{ |g| [(g.name || '').downcase, g.created_at] }
|
||||
|
||||
if authorized_action(@context, @current_user, :read_roster)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
if @context.grants_right?(@current_user, session, :manage_groups)
|
||||
format.html { render :action => 'context_manage_groups' }
|
||||
else
|
||||
format.html { render :action => 'context_groups' }
|
||||
end
|
||||
format.atom { render :xml => @groups.to_atom.to_xml }
|
||||
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>'
|
||||
#
|
||||
# @example_response
|
||||
# {
|
||||
# id: 13,
|
||||
# name: "Mary's Group",
|
||||
# description: "A group for my friends",
|
||||
# is_public: false,
|
||||
# join_level: "parent_context_request",
|
||||
# members_count: 3
|
||||
# group_category_id: 2
|
||||
# },
|
||||
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.select{|c| c.active? && c.users.include?(@current_user) } rescue []
|
||||
if params[:join] && @group.can_join?(@current_user)
|
||||
@group.request_user(@current_user)
|
||||
if !@group.grants_right?(@current_user, session, :read)
|
||||
render :action => '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.can_leave?(@current_user)
|
||||
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)
|
||||
@home_page = WikiNamespace.default_for_context(@group).wiki.wiki_page
|
||||
end
|
||||
end
|
||||
format.json do
|
||||
if authorized_action(@group, @current_user, :read)
|
||||
render :json => group_json(@group, @current_user, session)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -108,6 +226,163 @@ class GroupsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# @API Create a group
|
||||
#
|
||||
# Creates a new group. Right now, only community groups can be created
|
||||
# through the API.
|
||||
#
|
||||
# @argument name
|
||||
# @argument description
|
||||
# @argument is_public
|
||||
# @argument join_level
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id> \
|
||||
# -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>'
|
||||
#
|
||||
# @example_response
|
||||
# {
|
||||
# id: 25,
|
||||
# name: "Math Teachers",
|
||||
# description: "A place to gather resources for our classes.",
|
||||
# is_public: true,
|
||||
# join_level: "parent_context_auto_join",
|
||||
# members_count: 13
|
||||
# group_category_id: 7
|
||||
# }
|
||||
def create
|
||||
# only allow community groups from the api right now
|
||||
if api_request?
|
||||
@context = @domain_root_account
|
||||
params[:group_category] = GroupCategory.communities_for(@context)
|
||||
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.find_by_id(group_category_id)
|
||||
return render :json => {}, :status => :bad_request unless group_category
|
||||
params[:group][:group_category] = group_category
|
||||
else
|
||||
params[:group][:group_category] = nil
|
||||
end
|
||||
end
|
||||
|
||||
attrs = api_request? ? params : params[:group]
|
||||
@group = @context.groups.new(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?
|
||||
@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) }
|
||||
else
|
||||
format.html { render :action => "new" }
|
||||
format.json { render :json => @group.errors, :status => :bad_request }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
@context = @group
|
||||
if authorized_action(@group, @current_user, :update)
|
||||
end
|
||||
end
|
||||
|
||||
# @API Edit a group
|
||||
#
|
||||
# Modifies an existing group.
|
||||
#
|
||||
# @argument name
|
||||
# @argument description
|
||||
# @argument is_public Currently you cannot set a group back to private once
|
||||
# it has been made public.
|
||||
# @argument join_level
|
||||
#
|
||||
# @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>'
|
||||
#
|
||||
# @example_response
|
||||
# {
|
||||
# id: 25,
|
||||
# name: "Algebra Teachers",
|
||||
# description: "A place to gather resources for our classes.",
|
||||
# is_public: true,
|
||||
# join_level: "parent_context_request",
|
||||
# members_count: 13
|
||||
# group_category_id: 7
|
||||
# }
|
||||
def update
|
||||
find_group
|
||||
if !api_request? && params[:group] && params[:group][:group_category_id]
|
||||
group_category_id = params[:group].delete :group_category_id
|
||||
group_category = @context.group_categories.find_by_id(group_category_id)
|
||||
return render :json => {}, :status => :bad_request unless group_category
|
||||
params[:group][:group_category] = group_category
|
||||
end
|
||||
attrs = api_request? ? params : params[:group]
|
||||
if authorized_action(@group, @current_user, :update)
|
||||
respond_to do |format|
|
||||
if @group.update_attributes(attrs.slice(*SETTABLE_GROUP_ATTRIBUTES))
|
||||
flash[:notice] = t('notices.update_success', 'Group was successfully updated.')
|
||||
format.html { redirect_to group_url(@group) }
|
||||
format.json { render :json => group_json(@group, @current_user, session) }
|
||||
else
|
||||
format.html { render :action => "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>'
|
||||
#
|
||||
# @example_response
|
||||
# {
|
||||
# id: 144,
|
||||
# name: "My Group",
|
||||
# description: null,
|
||||
# is_public: false,
|
||||
# join_level: "invitation_only",
|
||||
# members_count: 0,
|
||||
# group_category_id: 9
|
||||
# }
|
||||
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
|
||||
|
||||
def create_category
|
||||
if authorized_action(@context, @current_user, :manage_groups)
|
||||
@group_category = @context.group_categories.build
|
||||
|
@ -169,84 +444,6 @@ class GroupsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def create
|
||||
if 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.find_by_id(group_category_id)
|
||||
return render :json => {}, :status => :bad_request unless group_category
|
||||
params[:group][:group_category] = group_category
|
||||
else
|
||||
params[:group][:group_category] = nil
|
||||
end
|
||||
end
|
||||
@group = @context.groups.build(params[:group])
|
||||
if authorized_action(@group, @current_user, :create)
|
||||
respond_to do |format|
|
||||
if @group.save
|
||||
if !@context.grants_right?(@current_user, session, :manage_groups)
|
||||
@group.add_user(@current_user)
|
||||
end
|
||||
@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.to_json(:methods => :participating_users_count) }
|
||||
else
|
||||
format.html { render :action => "new" }
|
||||
format.json { render :json => @group.errors.to_json }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
@context = @group
|
||||
if authorized_action(@group, @current_user, :update)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
if params[:group] && params[:group][:group_category_id]
|
||||
group_category_id = params[:group].delete :group_category_id
|
||||
group_category = @context.group_categories.find_by_id(group_category_id)
|
||||
return render :json => {}, :status => :bad_request unless group_category
|
||||
params[:group][:group_category] = group_category
|
||||
end
|
||||
if authorized_action(@group, @current_user, :manage)
|
||||
respond_to do |format|
|
||||
if @group.update_attributes(params[:group])
|
||||
flash[:notice] = t('notices.update_success', 'Group was successfully updated.')
|
||||
format.html { redirect_to group_url(@group) }
|
||||
format.json { render :json => @group.to_json(:methods => :participating_users_count) }
|
||||
else
|
||||
format.html { render :action => "edit" }
|
||||
format.json { render :json => @group.errors.to_json }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
if authorized_action(@group, @current_user, :manage)
|
||||
begin
|
||||
@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.to_json }
|
||||
end
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(dashboard_url) }
|
||||
format.json { render :json => @group.to_json, :status => :bad_request }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def public_feed
|
||||
return unless get_feed_context(:only => [:group])
|
||||
feed = Atom::Feed.new do |f|
|
||||
|
@ -325,45 +522,11 @@ class GroupsController < ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def get_group_as_context
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
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
|
||||
end
|
||||
|
||||
def context_index
|
||||
add_crumb (@context.is_a?(Account) ? t('#crumbs.users', "Users") : t('#crumbs.people', "People")), named_context_url(@context, :context_users_url)
|
||||
add_crumb t('#crumbs.groups', "Groups"), named_context_url(@context, :context_groups_url)
|
||||
@active_tab = @context.is_a?(Account) ? "users" : "people"
|
||||
@groups = @context.groups.active
|
||||
@categories = @context.group_categories
|
||||
group_ids = @groups.map(&:id)
|
||||
|
||||
@user_groups = @current_user.group_memberships_for(@context).select{|g| group_ids.include?(g.id) } if @current_user
|
||||
@user_groups ||= []
|
||||
|
||||
@available_groups = (@groups - @user_groups).select{|g| g.can_join?(@current_user) }
|
||||
if !@context.grants_right?(@current_user, session, :manage_groups)
|
||||
@groups = @user_groups
|
||||
end
|
||||
# sort by name, but with the student organized category in the back
|
||||
@categories = @categories.sort_by{|c| [ (c.student_organized? ? 1 : 0), c.name ] }
|
||||
@groups = @groups.sort_by{ |g| [(g.name || '').downcase, g.created_at] }
|
||||
|
||||
if authorized_action(@context, @current_user, :read_roster)
|
||||
respond_to do |format|
|
||||
if @context.grants_right?(@current_user, session, :manage_groups)
|
||||
format.html { render :action => 'context_manage_groups' }
|
||||
else
|
||||
format.html { render :action => 'context_groups' }
|
||||
end
|
||||
format.atom { render :xml => @groups.to_atom.to_xml }
|
||||
end
|
||||
def find_group
|
||||
if api_request?
|
||||
@group = Group.active.find(params[:group_id])
|
||||
else
|
||||
@group = (@context ? @context.groups : Group).find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ class Course < ActiveRecord::Base
|
|||
has_many :all_group_categories, :class_name => 'GroupCategory', :as => :context
|
||||
has_many :groups, :as => :context
|
||||
has_many :active_groups, :as => :context, :class_name => 'Group', :conditions => ['groups.workflow_state != ?', 'deleted']
|
||||
has_many :group_categories, :as => :context, :conditions => ['deleted_at IS NULL']
|
||||
has_many :assignment_groups, :as => :context, :dependent => :destroy, :order => 'assignment_groups.position, assignment_groups.name'
|
||||
has_many :assignments, :as => :context, :dependent => :destroy, :order => 'assignments.created_at'
|
||||
has_many :calendar_events, :as => :context, :conditions => ['calendar_events.workflow_state != ?', 'cancelled'], :dependent => :destroy
|
||||
|
|
|
@ -19,8 +19,11 @@
|
|||
class Group < ActiveRecord::Base
|
||||
include Context
|
||||
include Workflow
|
||||
include CustomValidations
|
||||
|
||||
attr_accessible :name, :context, :max_membership, :group_category, :join_level, :default_view, :description, :is_public
|
||||
validates_only_false_to_true :is_public
|
||||
|
||||
attr_accessible :name, :context, :max_membership, :group_category, :join_level, :default_view, :description
|
||||
has_many :group_memberships, :dependent => :destroy, :conditions => ['group_memberships.workflow_state != ?', 'deleted']
|
||||
has_many :users, :through => :group_memberships, :conditions => ['users.workflow_state != ?', 'deleted']
|
||||
has_many :participating_group_memberships, :class_name => "GroupMembership", :conditions => ['group_memberships.workflow_state = ?', 'accepted']
|
||||
|
@ -139,6 +142,10 @@ class Group < ActiveRecord::Base
|
|||
self.participating_group_memberships.moderators.find_by_user_id(user && user.id)
|
||||
end
|
||||
|
||||
def should_add_creator?
|
||||
self.group_category && (self.group_category.communities? || self.group_category.student_organized?)
|
||||
end
|
||||
|
||||
def short_name
|
||||
name
|
||||
end
|
||||
|
@ -199,9 +206,9 @@ class Group < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def add_user(user, new_record_state=nil)
|
||||
def add_user(user, new_record_state=nil, moderator=false)
|
||||
return nil if !user
|
||||
attrs = { :user => user }
|
||||
attrs = { :user => user, :moderator => moderator }
|
||||
new_record_state ||= case self.join_level
|
||||
when 'invitation_only' then 'invited'
|
||||
when 'parent_context_request' then 'requested'
|
||||
|
@ -268,6 +275,7 @@ class Group < ActiveRecord::Base
|
|||
self.group_category ||= GroupCategory.student_organized_for(self.context)
|
||||
self.join_level ||= 'invitation_only'
|
||||
self.is_public ||= false
|
||||
self.is_public = false unless self.group_category.try(:communities?)
|
||||
if self.context && self.context.is_a?(Course)
|
||||
self.account = self.context.account
|
||||
elsif self.context && self.context.is_a?(Account)
|
||||
|
@ -291,10 +299,8 @@ class Group < ActiveRecord::Base
|
|||
# permission check for efficiency -- see User#cached_contexts
|
||||
set_policy do
|
||||
given { |user| user && self.has_member?(user) }
|
||||
can :create and
|
||||
can :create_collaborations and
|
||||
can :create_conferences and
|
||||
can :delete and
|
||||
can :manage and
|
||||
can :manage_admin_users and
|
||||
can :manage_calendar and
|
||||
|
@ -306,7 +312,6 @@ class Group < ActiveRecord::Base
|
|||
can :read and
|
||||
can :read_roster and
|
||||
can :send_messages and
|
||||
can :update
|
||||
|
||||
# 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)
|
||||
|
@ -314,7 +319,12 @@ class Group < ActiveRecord::Base
|
|||
can :moderate_forum
|
||||
|
||||
given { |user| user && self.has_moderator?(user) }
|
||||
can :moderate_forum
|
||||
can :delete and
|
||||
can :moderate_forum and
|
||||
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) && self.context.allow_student_organized_groups }
|
||||
can :create
|
||||
|
@ -362,8 +372,8 @@ class Group < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def participating_users_count
|
||||
self.participating_users.count
|
||||
def members_count
|
||||
self.participating_group_memberships.count
|
||||
end
|
||||
|
||||
def quota
|
||||
|
|
|
@ -23,7 +23,7 @@ class GroupMembership < ActiveRecord::Base
|
|||
belongs_to :group
|
||||
belongs_to :user
|
||||
|
||||
attr_accessible :group, :user, :workflow_state
|
||||
attr_accessible :group, :user, :workflow_state, :moderator
|
||||
|
||||
before_save :ensure_mutually_exclusive_membership
|
||||
before_save :assign_uuid
|
||||
|
|
|
@ -1751,7 +1751,7 @@ class User < ActiveRecord::Base
|
|||
# according to the set_policy block in group.rb, user u can manage group
|
||||
# g if either:
|
||||
# (a) g.context.grants_right?(u, :manage_groups)
|
||||
# (b) g.participating_users.include(u)
|
||||
# (b) g.has_member?(u)
|
||||
# this is a very performance sensitive method, so we're bypassing the
|
||||
# normal policy checking and somewhat duplicating auth logic here. which
|
||||
# is a shame. it'd be really nice to add support to our policy framework
|
||||
|
|
|
@ -834,6 +834,7 @@ ActionController::Routing::Routes.draw do |map|
|
|||
end
|
||||
|
||||
api.with_options(:controller => :groups) do |groups|
|
||||
groups.resources :groups, :except => [:index]
|
||||
groups.post 'groups/:group_id/files', :action => :create_file
|
||||
groups.get 'groups/:group_id/activity_stream', :action => :activity_stream, :path_name => 'group_activity_stream'
|
||||
end
|
||||
|
|
|
@ -19,9 +19,14 @@
|
|||
module Api::V1::Group
|
||||
include Api::V1::Json
|
||||
|
||||
API_GROUP_JSON_OPTS = {
|
||||
:only => %w(id name description is_public join_level group_category_id),
|
||||
:methods => %w(members_count),
|
||||
}
|
||||
|
||||
def group_json(group, user, session, options = {})
|
||||
hash = api_json(group, user, session, API_GROUP_JSON_OPTS)
|
||||
include = options[:include] || []
|
||||
hash = api_json(group, user, session, :only => %w{id name})
|
||||
if include.include?('users')
|
||||
hash['users'] = group.users.map{ |u| user_json(u, user, session) }
|
||||
end
|
||||
|
|
|
@ -52,6 +52,15 @@ module CustomValidations
|
|||
end
|
||||
end
|
||||
|
||||
# only allow false -> true type changes
|
||||
def validates_only_false_to_true(*fields)
|
||||
validates_each(fields) do |record, attr, value|
|
||||
if !record.new_record? && record.send("#{attr}_changed?") && !!record.send("#{attr}_was") && !value
|
||||
record.errors.add attr, "cannot be changed back to false"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.included(klass)
|
||||
|
|
|
@ -46,11 +46,11 @@ define([
|
|||
$(this).slideUp();
|
||||
var $group = $("#group_blank").clone(true);
|
||||
$group.fillTemplateData({
|
||||
data: data.group || data.course_assigned_group,
|
||||
data: data,
|
||||
hrefValues: ['id']
|
||||
});
|
||||
$group.attr('id', "group_" + data.group.id)
|
||||
$group.find(".members_count").text(I18n.t('member', 'member', { count: data.group.participating_users_count }));
|
||||
$group.attr('id', "group_" + data.id)
|
||||
$group.find(".members_count").text(I18n.t('member', 'member', { count: data.members_count }));
|
||||
$("#group_blank").before($group.show());
|
||||
$("html,body").scrollTo($group);
|
||||
$group.animate({'backgroundColor': '#FFEE88'}, 1000)
|
||||
|
|
|
@ -389,20 +389,19 @@ define([
|
|||
return $group;
|
||||
},
|
||||
success: function(data, $group) {
|
||||
var group = data.group || data.course_assigned_group;
|
||||
$group.loadingImage('remove');
|
||||
$group.droppable(contextGroups.droppable_options);
|
||||
$group.find(".name.blank_name").hide().end()
|
||||
.find(".group_name").show();
|
||||
$group.find(".group_user_count").show();
|
||||
group.group_id = group.id;
|
||||
data.group_id = data.id;
|
||||
$group.fillTemplateData({
|
||||
id: 'group_' + group.id,
|
||||
data: group,
|
||||
id: 'group_' + data.id,
|
||||
data: data,
|
||||
avoid: '.student_list',
|
||||
hrefValues: ['id']
|
||||
});
|
||||
contextGroups.addGroupToSidebar(group)
|
||||
contextGroups.addGroupToSidebar(data)
|
||||
contextGroups.updateCategoryCounts($group.parents(".group_category"));
|
||||
}
|
||||
});
|
||||
|
@ -599,6 +598,7 @@ define([
|
|||
$("#group_tabs").tabs('add', '#category_' + group_category.id, group_category.name, newIndex);
|
||||
$("#group_tabs").tabs('select', newIndex);
|
||||
contextGroups.populateCategory($category);
|
||||
contextGroups.updateCategory($category, group_category);
|
||||
contextGroups.updateCategoryCounts($category);
|
||||
$(window).triggerHandler('resize');
|
||||
});
|
||||
|
|
|
@ -20,12 +20,112 @@ require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../file_uploads_spec_helper')
|
||||
|
||||
describe "Groups API", :type => :integration do
|
||||
def group_json(group)
|
||||
{
|
||||
'id' => group.id,
|
||||
'name' => group.name,
|
||||
'description' => group.description,
|
||||
'is_public' => group.is_public,
|
||||
'join_level' => group.join_level,
|
||||
'members_count' => group.members_count,
|
||||
'group_category_id' => group.group_category_id,
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
@moderator = user_model
|
||||
@member = user_with_pseudonym
|
||||
|
||||
@group_category = GroupCategory.communities_for(Account.default)
|
||||
group_model(:name => "Algebra Teachers", :group_category => @group_category)
|
||||
@group.add_user(@member, 'accepted', false)
|
||||
@group.add_user(@moderator, 'accepted', true)
|
||||
@group_path = "/api/v1/groups/#{@group.id}"
|
||||
@group_path_options = { :controller => "groups", :format => "json" }
|
||||
@group_json = group_json(@group)
|
||||
end
|
||||
|
||||
it "should allow a member to retrieve the group" do
|
||||
@user = @member
|
||||
json = api_call(:get, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "show"))
|
||||
json.should == @group_json
|
||||
end
|
||||
|
||||
it "should allow anyone to create a new community" do
|
||||
user_model
|
||||
json = api_call(:post, "/api/v1/groups", @group_path_options.merge(:action => "create"), {
|
||||
'name'=> "History Teachers",
|
||||
'description' => "Because history is awesome!",
|
||||
'is_public'=> false,
|
||||
'join_level'=> "parent_context_request",
|
||||
})
|
||||
@group2 = Group.last(:order => :id)
|
||||
@group2.group_category.should be_communities
|
||||
json.should == group_json(@group2)
|
||||
end
|
||||
|
||||
it "should allow a moderator to edit a group" do
|
||||
@user = @moderator
|
||||
new_attrs = {
|
||||
'name' => "Algebra II Teachers",
|
||||
'description' => "Math rocks!",
|
||||
'is_public' => true,
|
||||
'join_level' => "parent_context_auto_join",
|
||||
}
|
||||
json = api_call(:put, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "update"), new_attrs)
|
||||
@group.reload
|
||||
@group.name.should == "Algebra II Teachers"
|
||||
@group.description.should == "Math rocks!"
|
||||
@group.is_public.should == true
|
||||
@group.join_level.should == "parent_context_auto_join"
|
||||
json.should == @group_json.merge(new_attrs)
|
||||
end
|
||||
|
||||
it "should only allow updating a group from private to public" do
|
||||
@user = @moderator
|
||||
new_attrs = {
|
||||
'is_public' => true,
|
||||
}
|
||||
json = api_call(:put, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "update"), new_attrs)
|
||||
@group.reload
|
||||
@group.is_public.should == true
|
||||
|
||||
new_attrs = {
|
||||
'is_public' => false,
|
||||
}
|
||||
json = api_call(:put, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "update"), new_attrs, {}, :expected_status => 400)
|
||||
@group.reload
|
||||
@group.is_public.should == true
|
||||
end
|
||||
|
||||
it "should not allow a member to edit a group" do
|
||||
@user = @member
|
||||
new_attrs = {
|
||||
'name'=> "Algebra II Teachers",
|
||||
'is_public'=> true,
|
||||
'join_level'=> "parent_context_auto_join",
|
||||
}
|
||||
json = api_call(:put, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "update"), new_attrs, {}, :expected_status => 401)
|
||||
json['message'].should match /not authorized/
|
||||
end
|
||||
|
||||
it "should allow a moderator to delete a group" do
|
||||
@user = @moderator
|
||||
json = api_call(:delete, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "destroy"))
|
||||
json.should == @group_json.merge({ 'members_count' => 0 })
|
||||
end
|
||||
|
||||
it "should not allow a member to delete a group" do
|
||||
@user = @member
|
||||
json = api_call(:delete, @group_path, @group_path_options.merge(:group_id => @group.to_param, :action => "destroy"), {}, {}, :expected_status => 401)
|
||||
json['message'].should match /not authorized/
|
||||
end
|
||||
|
||||
context "group files" do
|
||||
it_should_behave_like "file uploads api with folders"
|
||||
|
||||
before do
|
||||
group_model
|
||||
@group.add_user(user_with_pseudonym)
|
||||
@user = @member
|
||||
end
|
||||
|
||||
def preflight(preflight_params)
|
||||
|
|
|
@ -412,7 +412,7 @@ describe GroupsController do
|
|||
describe "POST create" do
|
||||
it "should require authorization" do
|
||||
course_with_teacher(:active_all => true)
|
||||
post 'create', :course_id => @course.id
|
||||
post 'create', :course_id => @course.id, :group => {:name => "some group"}
|
||||
assert_unauthorized
|
||||
end
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
|||
describe Group do
|
||||
|
||||
before do
|
||||
group_model
|
||||
course_model
|
||||
group_model(:context => @course)
|
||||
end
|
||||
|
||||
context "validation" do
|
||||
|
@ -37,6 +38,22 @@ describe Group do
|
|||
it "should be private by default" do
|
||||
@group.is_public.should be_false
|
||||
end
|
||||
|
||||
it "should allow a private group to be made public" do
|
||||
@communities = GroupCategory.communities_for(Account.default)
|
||||
group_model(:group_category => @communities, :is_public => false)
|
||||
@group.is_public = true
|
||||
@group.save!
|
||||
@group.reload.is_public.should be_true
|
||||
end
|
||||
|
||||
it "should not allow a public group to be made private" do
|
||||
@communities = GroupCategory.communities_for(Account.default)
|
||||
group_model(:group_category => @communities, :is_public => true)
|
||||
@group.is_public = false
|
||||
@group.save.should be_false
|
||||
@group.reload.is_public.should be_true
|
||||
end
|
||||
|
||||
context "#peer_groups" do
|
||||
it "should find all peer groups" do
|
||||
|
|
Loading…
Reference in New Issue