add collections to groups (backend + api); closes #8670

groups are now a valid context for collections. as a group moderator, you are
allowed to do anything (create/update/delete) with any collection or item in
the group. as a group member, you are not allowed to create/update/delete
collections, but you are allowed to post to them, and edit/delete posts you
have made.

test plan:
- nothing visible, but try making calls through the api. make sure the above
  permissions are accurately represented
- make sure the api doc additions are coherent

Change-Id: Ic259c6bdf1988420438a8258bf585671f972cd32
Reviewed-on: https://gerrit.instructure.com/10848
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Simon Williams 2012-05-17 15:48:07 -06:00
parent 2b1a7b74fb
commit efe769d5df
8 changed files with 422 additions and 239 deletions

View File

@ -166,7 +166,7 @@ class CollectionItemsController < ApplicationController
# }
def show
find_item_and_collection
if authorized_action(@collection, @current_user, :read)
if authorized_action(@item, @current_user, :read)
render :json => collection_items_json([@item], @current_user, session).first
end
end
@ -206,11 +206,12 @@ class CollectionItemsController < ApplicationController
# -H 'Authorization: Bearer <token>'
#
def create
if authorized_action(@collection, @current_user, :update)
@item = @collection.collection_items.new(:user => @current_user)
if authorized_action(@item, @current_user, :create)
item_data = CollectionItemData.data_for_url(params[:link_url] || "", @current_user)
return render_unauthorized_action unless item_data
item_data.image_url = params[:image_url] if item_data.new_record?
@item = @collection.collection_items.new(:collection_item_data => item_data, :user => @current_user)
@item.collection_item_data = item_data
@item.attributes = params.slice(*ITEM_SETTABLE_ATTRIBUTES)
if @item.errors.empty? && @item.save
@ -238,7 +239,7 @@ class CollectionItemsController < ApplicationController
#
def update
find_item_and_collection
if authorized_action(@collection, @current_user, :update)
if authorized_action(@item, @current_user, :update)
if @item.update_attributes(params.slice(*ITEM_SETTABLE_ATTRIBUTES))
render :json => collection_items_json([@item], @current_user, session).first
else
@ -260,7 +261,7 @@ class CollectionItemsController < ApplicationController
# -H 'Authorization: Bearer <token>'
def destroy
find_item_and_collection
if authorized_action(@collection, @current_user, :update)
if authorized_action(@item, @current_user, :delete)
if @item.destroy
render :json => collection_items_json([@item], @current_user, session).first
else
@ -296,7 +297,7 @@ class CollectionItemsController < ApplicationController
# }
def upvote
find_item_and_collection
if authorized_action(@collection, @current_user, :read)
if authorized_action(@item, @current_user, :read)
@upvote = find_upvote
@upvote ||= @item.collection_item_data.collection_item_upvotes.create!({ :user => @current_user })
render :json => collection_item_upvote_json(@item, @upvote, @current_user, session)
@ -316,7 +317,7 @@ class CollectionItemsController < ApplicationController
# -H 'Authorization: Bearer <token>'
def remove_upvote
find_item_and_collection
if authorized_action(@collection, @current_user, :read)
if authorized_action(@item, @current_user, :read)
@upvote = find_upvote
if @upvote
@upvote.destroy

View File

@ -22,10 +22,15 @@
#
# Collections are buckets of content that can be used to organize links to
# helpful resources. For instance, a user could create a collection storing a
# set of links to various web sites containing potential discussion questions.
# set of links to various web sites containing potential discussion questions,
# or members of a group could all contribute to a collection focused on
# potential assessment questions.
#
# A user can have multiple collections, and each can be marked as private
# (viewable only to the user) or public (viewable by the world).
# A user/group can have multiple collections, and each can be marked as private
# (viewable only to the user/group) or public (viewable by the world).
#
# Group collections can only be created, updated, or deleted by group
# moderators.
#
# A Collection object looks like:
#
@ -109,7 +114,8 @@ class CollectionsController < ApplicationController
# @API Create a collection
#
# Creates a new collection. You can only create collections on your own user.
# Creates a new collection. You can only create collections on your own user,
# or on a group to which you belong.
#
# @argument name
# @argument visibility

View File

@ -53,5 +53,11 @@ class Collection < ActiveRecord::Base
given { |user| self.context == user }
can :read and can :create and can :update and can :delete and can :comment
given { |user| self.context.respond_to?(:has_member?) && self.context.has_member?(user) }
can :read and can :comment
given { |user| self.context.respond_to?(:has_moderator?) && self.context.has_moderator?(user) }
can :read and can :create and can :update and can :delete and can :comment
end
end

View File

@ -101,10 +101,19 @@ class CollectionItem < ActiveRecord::Base
given { |user, session| self.collection.grants_right?(user, session, :comment) }
can :comment
given { |user, session| self.collection.grants_right?(user, session, :create) }
can :create
given { |user, session| self.collection.grants_right?(user, session, :delete) }
can :delete
given { |user, session| self.collection.grants_right?(user, session, :update) }
can :update
given { |user| self.user == user }
can :read and can :update and can :delete
given { |user| self.collection.context.respond_to?(:has_member?) && self.collection.context.has_member?(user) }
can :create
end
end

View File

@ -58,6 +58,7 @@ class Group < ActiveRecord::Base
has_many :short_messages, :through => :short_message_associations, :dependent => :destroy
has_many :media_objects, :as => :context
has_many :zip_file_imports, :as => :context
has_many :collections, :as => :context
before_save :ensure_defaults, :maintain_category_attribute
after_save :close_memberships_if_deleted

View File

@ -91,6 +91,10 @@ class GroupCategory < ActiveRecord::Base
self.role.present?
end
# Group categories generally restrict students to only be in one group per
# category, but we sort of cheat and implement student organized groups and
# communities as one big group category, and then relax that membership
# restriction.
def allows_multiple_memberships?
self.student_organized? || self.communities?
end

View File

@ -840,6 +840,7 @@ ActionController::Routing::Routes.draw do |map|
api.with_options(:controller => :collections) do |collections|
collections.resources :collections, :path_prefix => "users/:user_id", :name_prefix => "user_"
collections.resources :collections, :path_prefix => "groups/:group_id", :name_prefix => "group_"
collections.with_options(:controller => :collection_items) do |items|
items.get "collections/:collection_id/items", :action => :index, :path_name => 'collection_items_list'

View File

@ -19,27 +19,7 @@
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
describe "Collections API", :type => :integration do
before do
user_with_pseudonym
@collections_path = "/api/v1/users/#{@user.id}/collections"
@collections_path_options = { :controller => "collections", :action => "index", :format => "json", :user_id => @user.to_param }
@c1 = @user.collections.create!(:name => 'test1', :visibility => 'private')
@c2 = @user.collections.create!(:name => 'test2', :visibility => 'public')
@c1_json =
{
'id' => @c1.id,
'name' => @c1.name,
'visibility' => 'private',
}
@c2_json =
{
'id' => @c2.id,
'name' => @c2.name,
'visibility' => 'public',
}
end
context "a user's own collections" do
shared_examples_for "full access to collections" do
it "should allow retrieving a paginated collection list" do
json = api_call(:get, @collections_path, @collections_path_options)
response['Link'].should be_present
@ -102,7 +82,7 @@ describe "Collections API", :type => :integration do
end
end
context "another user's collections" do
shared_examples_for "public only access to collections" do
before do
user_with_pseudonym
end
@ -123,6 +103,15 @@ describe "Collections API", :type => :integration do
json['message'].should match /not authorized/
end
it "should not allow creating a collection" do
expect {
json = api_call(:post, @collections_path, @collections_path_options.merge(:action => "create"), {
:name => "test3",
:visibility => 'public',
}, {}, :expected_status => 401)
}.to change(CollectionItem, :count).by(0)
end
it "should not allow updating a collection" do
json = api_call(:put, @collections_path + "/#{@c2.id}", @collections_path_options.merge(:collection_id => @c2.to_param, :action => "update"), {
:name => "test2 edited",
@ -136,234 +125,400 @@ describe "Collections API", :type => :integration do
end
end
describe "Collection Items" do
before do
@i1 = collection_item_model(:description => "item 1", :user => @c1.context, :collection => @c1, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/one"))
@i2 = collection_item_model(:description => "item 2", :user => @c1.context, :collection => @c1, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/two"))
@i3 = collection_item_model(:description => "item 3", :user => @c2.context, :collection => @c2, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/three"))
@items1_path = "/api/v1/collections/#{@c1.id}/items"
@items2_path = "/api/v1/collections/#{@c2.id}/items"
@items1_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c1.to_param }
@items2_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c2.to_param }
@user1 = @user
user_with_pseudonym
@user2 = @user
@user = @user1
@c3 = @user2.collections.create!(:name => 'user2', :visibility => 'public')
@i4 = collection_item_model(:description => "cloned item 3", :user => @c3.context, :collection => @c3, :collection_item_data => @i3.collection_item_data); @i3.reload
@items3_path = "/api/v1/collections/#{@c3.id}/items"
@items3_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c3.to_param }
end
def item_json(item, upvoted_by_user = false)
def create_collections(context)
@c1 = context.collections.create!(:name => 'test1', :visibility => 'private')
@c2 = context.collections.create!(:name => 'test2', :visibility => 'public')
@c1_json =
{
'id' => item.id,
'collection_id' => item.collection_id,
'item_type' => item.collection_item_data.item_type,
'link_url' => item.collection_item_data.link_url,
'post_count' => item.collection_item_data.post_count,
'upvote_count' => item.collection_item_data.upvote_count,
'upvoted_by_user' => upvoted_by_user,
'root_item_id' => item.collection_item_data.root_item_id,
'image_url' => item.data.image_attachment && "http://www.example.com/images/thumbnails/#{item.data.image_attachment.id}/#{item.data.image_attachment.uuid}?size=640x%3E",
'image_pending' => item.data.image_pending,
'html_preview' => item.data.html_preview,
'description' => item.description,
'url' => "http://www.example.com/api/v1/collections/items/#{item.id}",
'id' => @c1.id,
'name' => @c1.name,
'visibility' => 'private',
}
@c2_json =
{
'id' => @c2.id,
'name' => @c2.name,
'visibility' => 'public',
}
end
def create_collection_items(user)
@i1 = collection_item_model(:description => "item 1", :user => user, :collection => @c1, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/one"))
@i2 = collection_item_model(:description => "item 2", :user => user, :collection => @c1, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/two"))
@i3 = collection_item_model(:description => "item 3", :user => user, :collection => @c2, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/three"))
@unscoped_items_path = "/api/v1/collections/items"
@c1_items_path = "/api/v1/collections/#{@c1.id}/items"
@c2_items_path = "/api/v1/collections/#{@c2.id}/items"
@unscoped_items_path_options = { :controller => "collection_items", :action => "index", :format => "json" }
@c1_items_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c1.to_param }
@c2_items_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c2.to_param }
end
def item_json(item, upvoted_by_user = false)
{
'id' => item.id,
'collection_id' => item.collection_id,
'item_type' => item.collection_item_data.item_type,
'link_url' => item.collection_item_data.link_url,
'post_count' => item.collection_item_data.post_count,
'upvote_count' => item.collection_item_data.upvote_count,
'upvoted_by_user' => upvoted_by_user,
'root_item_id' => item.collection_item_data.root_item_id,
'image_url' => item.data.image_attachment && "http://www.example.com/images/thumbnails/#{item.data.image_attachment.id}/#{item.data.image_attachment.uuid}?size=640x%3E",
'image_pending' => item.data.image_pending,
'html_preview' => item.data.html_preview,
'description' => item.description,
'url' => "http://www.example.com/api/v1/collections/items/#{item.id}",
}
end
context "user scoped collections" do
before do
user_with_pseudonym
@collections_path = "/api/v1/users/#{@user.id}/collections"
@collections_path_options = { :controller => "collections", :action => "index", :format => "json", :user_id => @user.to_param }
create_collections(@user)
end
it "should allow retrieving a pagniated item list from a private collection" do
json = api_call(:get, @items1_path, @items1_path_options)
response['Link'].should be_present
json.should == [ item_json(@i2), item_json(@i1) ]
end
describe "item creation" do
it "should allow creating from a http url" do
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
new_item = @c1.collection_items.last(:order => :id)
new_item.collection_item_data.link_url.should == "http://www.example.com/a/b/c"
new_item.user.should == @user
end
it "should allow cloning an existing item" do
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "http://localhost/api/v1/collections/items/#{@i3.id}", :description => 'cloned' })
json['post_count'].should == 3
new_item = @c1.collection_items.last(:order => :id)
new_item.collection_item_data.should == @i3.collection_item_data
new_item.user.should == @user
end
it "should not allow cloning an item the user can't access" do
@user = @user2
expect {
json = api_call(:post, @items3_path, @items3_path_options.merge(:action => "create"), { :link_url => "http://localhost/api/v1/collections/items/#{@i1.id}", :description => 'cloned' }, {}, :expected_status => 401)
}.to change(CollectionItem, :count).by(0)
end
it "should reject non-http urls" do
expect {
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "javascript:alert(1)", :description => 'new item' }, {}, :expected_status => 400)
}.to change(CollectionItem, :count).by(0)
end
describe "images" do
it "should take a snapshot of the link url if no image is provided and there is no embedly image" do
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
@att = Attachment.new(:uploaded_data => stub_png_data)
CutyCapt.expects(:snapshot_attachment_for_url).with(@item.data.link_url).returns(@att)
run_job()
@att.reload.context.should == Account.default
@item.reload.data.image_pending.should == false
@item.data.image_attachment.should == @att
json = api_call(:get, "/api/v1/collections/items/#{@item.id}", { :controller => "collection_items", :item_id => @item.to_param, :action => "show", :format => "json" })
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
it "should clone and use the image if provided" do
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :image_url => "http://www.example.com/my/image.png", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
http_res = mock('Net::HTTPOK', :body => File.read(Rails.root+"public/images/cancel.png"), :code => 200)
Canvas::HTTP.expects(:get).with("http://www.example.com/my/image.png").returns(http_res)
run_job()
@item.reload.data.image_pending.should == false
@att = @item.data.image_attachment
@att.should be_present
@att.context.should == Account.default
json = api_call(:get, "/api/v1/collections/items/#{@item.id}", { :controller => "collection_items", :item_id => @item.to_param, :action => "show", :format => "json" })
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
it "should use the embedly image if no image is provided" do
json = api_call(:post, @items1_path, @items1_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
Canvas::Embedly.any_instance.expects(:get_embedly_data).with("http://www.example.com/a/b/c").returns(stub_everything('embedly api', :type => 'test', :images => [{'url' => 'http://www.example.com/image1'}], :html => "<iframe>test</iframe>"))
http_res = mock('Net::HTTPOK', :body => File.read(Rails.root+"public/images/cancel.png"), :code => 200)
Canvas::HTTP.expects(:get).with("http://www.example.com/image1").returns(http_res)
run_job()
@item.reload.data.image_pending.should == false
@att = @item.data.image_attachment
@att.should be_present
@att.context.should == Account.default
@item.data.html_preview.should == "<iframe>test</iframe>"
json = api_call(:get, "/api/v1/collections/items/#{@item.id}", { :controller => "collection_items", :item_id => @item.to_param, :action => "show", :format => "json" })
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
end
end
it "should allow editing mutable fields" do
json = api_call(:put, "/api/v1/collections/items/#{@i1.id}", { :controller => "collection_items", :item_id => @i1.to_param, :action => "update", :format => "json" }, { :description => "modified", :link_url => 'cant change', :item_type => 'cant change', :image_url => "http://www.example.com/cant_change" })
json.should == item_json(@i1.reload)
@i1.description.should == "modified"
@i1.collection_item_data.item_type.should == "url"
@i1.data.image_pending.should == false
end
it "should allow deleting an owned item" do
json = api_call(:delete, "/api/v1/collections/items/#{@i1.id}", { :controller => "collection_items", :item_id => @i1.to_param, :action => "destroy", :format => "json" })
@i1.reload.state.should == :deleted
end
it "should not allow getting from a deleted collection" do
@i1.collection.destroy
# deleting the collection doesn't mark all the items as deleted, though
# they can't be retrieved through the api
# this makes undeleting work better
@i1.reload.should be_active
json = api_call(:get, "/api/v1/collections/items/#{@i1.id}", { :controller => "collection_items", :item_id => @i1.to_param, :action => "show", :format => "json" }, {}, {}, :expected_status => 404)
end
context "deleted item" do
before do
@i1.destroy
end
it "should not return in the list" do
json = api_call(:get, @items1_path, @items1_path_options)
json.should == [ item_json(@i2) ]
end
it "should not allow getting" do
json = api_call(:get, "/api/v1/collections/items/#{@i1.id}", { :controller => "collection_items", :item_id => @i1.to_param, :action => "show", :format => "json" }, {}, {}, :expected_status => 404)
end
context "a user's own collections" do
it_should_behave_like "full access to collections"
end
context "another user's collections" do
it_should_behave_like "public only access to collections"
end
describe "Collection Items" do
before do
create_collection_items(@user)
@user1 = @user
user_with_pseudonym
@user2 = @user
@user = @user1
@c3 = @user2.collections.create!(:name => 'user2', :visibility => 'public')
@i4 = collection_item_model(:description => "cloned item 3", :user => @c3.context, :collection => @c3, :collection_item_data => @i3.collection_item_data); @i3.reload
@items3_path = "/api/v1/collections/#{@c3.id}/items"
@items3_path_options = { :controller => "collection_items", :action => "index", :format => "json", :collection_id => @c3.to_param }
end
it "should not allow listing from a private collection" do
json = api_call(:get, @items1_path, @items1_path_options, {}, {}, :expected_status => 401)
end
it "should allow listing a public collection" do
json = api_call(:get, @items2_path, @items2_path_options)
it "should allow retrieving a pagniated item list from a private collection" do
json = api_call(:get, @c1_items_path, @c1_items_path_options)
response['Link'].should be_present
json.should == [ item_json(@i3) ]
json.should == [ item_json(@i2), item_json(@i1) ]
end
describe "item creation" do
it "should allow creating from a http url" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
new_item = @c1.collection_items.last(:order => :id)
new_item.collection_item_data.link_url.should == "http://www.example.com/a/b/c"
new_item.user.should == @user
end
it "should allow cloning an existing item" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://localhost/api/v1/collections/items/#{@i3.id}", :description => 'cloned' })
json['post_count'].should == 3
new_item = @c1.collection_items.last(:order => :id)
new_item.collection_item_data.should == @i3.collection_item_data
new_item.user.should == @user
end
it "should not allow cloning an item the user can't access" do
@user = @user2
expect {
json = api_call(:post, @items3_path, @items3_path_options.merge(:action => "create"), { :link_url => "http://localhost/api/v1/collections/items/#{@i1.id}", :description => 'cloned' }, {}, :expected_status => 401)
}.to change(CollectionItem, :count).by(0)
end
it "should reject non-http urls" do
expect {
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "javascript:alert(1)", :description => 'new item' }, {}, :expected_status => 400)
}.to change(CollectionItem, :count).by(0)
end
describe "images" do
it "should take a snapshot of the link url if no image is provided and there is no embedly image" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
@att = Attachment.new(:uploaded_data => stub_png_data)
CutyCapt.expects(:snapshot_attachment_for_url).with(@item.data.link_url).returns(@att)
run_job()
@att.reload.context.should == Account.default
@item.reload.data.image_pending.should == false
@item.data.image_attachment.should == @att
json = api_call(:get, "#{@unscoped_items_path}/#{@item.id}", @unscoped_items_path_options.merge(:item_id => @item.to_param, :action => "show"))
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
it "should clone and use the image if provided" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :image_url => "http://www.example.com/my/image.png", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
http_res = mock('Net::HTTPOK', :body => File.read(Rails.root+"public/images/cancel.png"), :code => 200)
Canvas::HTTP.expects(:get).with("http://www.example.com/my/image.png").returns(http_res)
run_job()
@item.reload.data.image_pending.should == false
@att = @item.data.image_attachment
@att.should be_present
@att.context.should == Account.default
json = api_call(:get, "#{@unscoped_items_path}/#{@item.id}", @unscoped_items_path_options.merge(:item_id => @item.to_param, :action => "show"))
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
it "should use the embedly image if no image is provided" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
@item = CollectionItem.find(json['id'])
@item.data.image_pending.should == true
Canvas::Embedly.any_instance.expects(:get_embedly_data).with("http://www.example.com/a/b/c").returns(stub_everything('embedly api', :type => 'test', :images => [{'url' => 'http://www.example.com/image1'}], :html => "<iframe>test</iframe>"))
http_res = mock('Net::HTTPOK', :body => File.read(Rails.root+"public/images/cancel.png"), :code => 200)
Canvas::HTTP.expects(:get).with("http://www.example.com/image1").returns(http_res)
run_job()
@item.reload.data.image_pending.should == false
@att = @item.data.image_attachment
@att.should be_present
@att.context.should == Account.default
@item.data.html_preview.should == "<iframe>test</iframe>"
json = api_call(:get, "#{@unscoped_items_path}/#{@item.id}", @unscoped_items_path_options.merge(:item_id => @item.to_param, :action => "show"))
json['image_pending'].should == false
json['image_url'].should == "http://www.example.com/images/thumbnails/#{@att.id}/#{@att.uuid}?size=640x%3E"
end
end
end
it "should allow editing mutable fields" do
json = api_call(:put, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "update"), {
:description => "modified",
:link_url => 'cant change',
:item_type => 'cant change',
:image_url => "http://www.example.com/cant_change"
})
json.should == item_json(@i1.reload)
@i1.description.should == "modified"
@i1.collection_item_data.item_type.should == "url"
@i1.data.image_pending.should == false
end
it "should allow deleting an owned item" do
json = api_call(:delete, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "destroy"))
@i1.reload.state.should == :deleted
end
it "should not allow getting from a deleted collection" do
@i1.collection.destroy
# deleting the collection doesn't mark all the items as deleted, though
# they can't be retrieved through the api
# this makes undeleting work better
@i1.reload.should be_active
json = api_call(:get, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "show"), {}, {}, :expected_status => 404)
end
context "deleted item" do
before do
@i1.destroy
end
it "should not return in the list" do
json = api_call(:get, @c1_items_path, @c1_items_path_options)
json.should == [ item_json(@i2) ]
end
it "should not allow getting" do
json = api_call(:get, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "show"), {}, {}, :expected_status => 404)
end
end
context "another user's collections" do
before do
user_with_pseudonym
end
it "should not allow listing from a private collection" do
json = api_call(:get, @c1_items_path, @c1_items_path_options, {}, {}, :expected_status => 401)
end
it "should allow listing a public collection" do
json = api_call(:get, @c2_items_path, @c2_items_path_options)
response['Link'].should be_present
json.should == [ item_json(@i3) ]
end
end
context "upvoting" do
it "should allow upvoting an item" do
@user = @user2
json = api_call(:put, "#{@unscoped_items_path}/#{@i3.id}/upvote", @unscoped_items_path_options.merge(:action => "upvote", :item_id => @i3.to_param))
json.slice('item_id', 'root_item_id', 'user_id').should == {
'item_id' => @i3.id,
'root_item_id' => @i3.id,
'user_id' => @user.id,
}
@i3.reload.collection_item_data.upvote_count.should == 1
# upvoting again is a no-op
json = api_call(:put, "#{@unscoped_items_path}/#{@i3.id}/upvote", @unscoped_items_path_options.merge(:action => "upvote", :item_id => @i3.to_param))
json.slice('item_id', 'root_item_id', 'user_id').should == {
'item_id' => @i3.id,
'root_item_id' => @i3.id,
'user_id' => @user.id,
}
@i3.reload.collection_item_data.upvote_count.should == 1
end
it "should not allow upvoting a non-visible item" do
@user = @user2
json = api_call(:put, "#{@unscoped_items_path}/#{@i1.id}/upvote", @unscoped_items_path_options.merge(:action => "upvote", :item_id => @i1.to_param), {}, {}, :expected_status => 401)
@i1.reload.collection_item_data.upvote_count.should == 0
end
end
context "de-upvoting" do
before do
@user = @user2
end
it "should allow removing an upvote" do
@i3.collection_item_data.collection_item_upvotes.create!(:user => @user)
@i3.reload.collection_item_data.upvote_count.should == 1
json = api_call(:delete, "#{@unscoped_items_path}/#{@i3.id}/upvote", @unscoped_items_path_options.merge(:action => "remove_upvote", :item_id => @i3.to_param))
@i3.reload.collection_item_data.upvote_count.should == 0
end
it "should ignore if the user hasn't upvoted the item" do
json = api_call(:delete, "#{@unscoped_items_path}/#{@i3.id}/upvote", @unscoped_items_path_options.merge(:action => "remove_upvote", :item_id => @i3.to_param))
end
end
end
end
context "upvoting" do
it "should allow upvoting an item" do
@user = @user2
json = api_call(:put, "/api/v1/collections/items/#{@i3.id}/upvote", { :controller => "collection_items", :action => "upvote", :item_id => @i3.to_param, :format => "json" })
json.slice('item_id', 'root_item_id', 'user_id').should == {
'item_id' => @i3.id,
'root_item_id' => @i3.id,
'user_id' => @user.id,
}
@i3.reload.collection_item_data.upvote_count.should == 1
# upvoting again is a no-op
json = api_call(:put, "/api/v1/collections/items/#{@i3.id}/upvote", { :controller => "collection_items", :action => "upvote", :item_id => @i3.to_param, :format => "json" })
json.slice('item_id', 'root_item_id', 'user_id').should == {
'item_id' => @i3.id,
'root_item_id' => @i3.id,
'user_id' => @user.id,
}
@i3.reload.collection_item_data.upvote_count.should == 1
end
it "should not allow upvoting a non-visible item" do
@user = @user2
json = api_call(:put, "/api/v1/collections/items/#{@i1.id}/upvote", { :controller => "collection_items", :action => "upvote", :item_id => @i1.to_param, :format => "json" }, {}, {}, :expected_status => 401)
@i1.reload.collection_item_data.upvote_count.should == 0
end
context "group scoped collections" do
before do
user_with_pseudonym
group_model({:group_category => GroupCategory.communities_for(Account.default), :is_public => true})
@collections_path = "/api/v1/groups/#{@group.id}/collections"
@collections_path_options = { :controller => "collections", :action => "index", :format => "json", :group_id => @group.to_param }
create_collections(@group)
end
context "de-upvoting" do
context "a group's collections, as moderator" do
before do
@user = @user2
@group_membership = @group.add_user(@user)
@group_membership.moderator = true
@group_membership.save!
end
it "should allow removing an upvote" do
@i3.collection_item_data.collection_item_upvotes.create!(:user => @user)
@i3.reload.collection_item_data.upvote_count.should == 1
json = api_call(:delete, "/api/v1/collections/items/#{@i3.id}/upvote", { :controller => "collection_items", :action => "remove_upvote", :item_id => @i3.to_param, :format => "json" })
@i3.reload.collection_item_data.upvote_count.should == 0
it_should_behave_like "full access to collections"
end
context "a group's collections, as member" do
before do
@group_membership = @group.add_user(@user)
end
it "should ignore if the user hasn't upvoted the item" do
json = api_call(:delete, "/api/v1/collections/items/#{@i3.id}/upvote", { :controller => "collection_items", :action => "remove_upvote", :item_id => @i3.to_param, :format => "json" })
it "should allow retrieving a paginated collection list" do
json = api_call(:get, @collections_path, @collections_path_options)
response['Link'].should be_present
json.should == [ @c2_json, @c1_json ]
end
it "should allow retrieving a private collection" do
json = api_call(:get, @collections_path + "/#{@c1.id}", @collections_path_options.merge(:collection_id => @c1.to_param, :action => "show"))
json.should == @c1_json
end
it "should not allow creating a collection" do
expect {
json = api_call(:post, @collections_path, @collections_path_options.merge(:action => "create"), {
:name => "test3",
:visibility => 'public',
}, {}, :expected_status => 401)
}.to change(CollectionItem, :count).by(0)
end
it "should not allow updating a collection" do
json = api_call(:put, @collections_path + "/#{@c2.id}", @collections_path_options.merge(:collection_id => @c2.to_param, :action => "update"), {
:name => "test2 edited",
}, {}, :expected_status => 401)
@c2.reload.name.should == "test2"
end
it "should not allow deleting a collection" do
json = api_call(:delete, @collections_path + "/#{@c2.id}", @collections_path_options.merge(:collection_id => @c2.to_param, :action => "destroy"), {}, {}, :expected_status => 401)
@c2.reload.should be_active
end
end
context "a group's collections, as a non-member" do
it_should_behave_like "public only access to collections"
end
describe "Collection Items" do
before do
create_collection_items(@user)
@user1 = @user
@user2 = user_with_pseudonym
@group_membership2 = @group.add_user(@user2)
@i4 = collection_item_model(:description => "item 4", :user => @user2, :collection => @c2, :collection_item_data => collection_item_data_model(:link_url => "http://www.example.com/three"))
end
context "as a group member" do
before do
@group_membership = @group.add_user(@user1)
@group_membership2 = @group.add_user(@user2)
@user = @user2
end
it "should allow creating a new item" do
json = api_call(:post, @c1_items_path, @c1_items_path_options.merge(:action => "create"), { :link_url => "http://www.example.com/a/b/c", :description => 'new item' })
new_item = @c1.collection_items.last(:order => :id)
new_item.collection_item_data.link_url.should == "http://www.example.com/a/b/c"
new_item.user.should == @user
end
it "should not allow updating an item created by someone else" do
orig_description = @i1.description
json = api_call(:put, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "update"), {
:description => "item1 description edited",
}, {}, :expected_status => 401)
@i1.reload.description.should == orig_description
end
it "should not allow deleting an item created by someone else" do
json = api_call(:delete, "#{@unscoped_items_path}/#{@i1.id}", @unscoped_items_path_options.merge(:item_id => @i1.to_param, :action => "destroy"), {}, {}, :expected_status => 401)
@i1.reload.should be_active
end
end
context "as a group moderator" do
before do
@group_membership = @group.add_user(@user1)
@group_membership.moderator = true
@group_membership.save!
@group_membership2 = @group.add_user(@user2)
@user = @user1
end
it "should allow updating an item created by someone else" do
json = api_call(:put, "#{@unscoped_items_path}/#{@i4.id}", @unscoped_items_path_options.merge(:item_id => @i4.to_param, :action => "update"), { :description => "modified" })
json.should == item_json(@i4.reload)
@i4.description.should == "modified"
end
it "should allow deleting an item created by someone else" do
json = api_call(:delete, "#{@unscoped_items_path}/#{@i4.id}", @unscoped_items_path_options.merge(:item_id => @i4.to_param, :action => "destroy"))
@i4.reload.should be_deleted
end
end
end
end