add search_term helper for api indexes

test plan:
* test that the 'search_term' argument can be used
(as documented) in the api 'index' actions for the following:
 - assignments
 - discussion topics
 - external tools
 - quizzes

and they behave identically to the wiki pages api

closes #CNVS-7047

Change-Id: I133a9571f134563deb06cd62956238d1e4a57d22
Reviewed-on: https://gerrit.instructure.com/22587
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
Product-Review: Jeremy Stanley <jeremy@instructure.com>
QA-Review: August Thornton <august@instructure.com>
This commit is contained in:
James Williams 2013-07-23 12:09:33 -06:00
parent 2dcf0adb32
commit 9172c8c5ca
13 changed files with 112 additions and 1 deletions

View File

@ -291,6 +291,7 @@ class AssignmentsApiController < ApplicationController
# @API List assignments # @API List assignments
# Returns the list of assignments for the current context. # Returns the list of assignments for the current context.
# @argument include[] ["submission"] Associations to include with the assignment. # @argument include[] ["submission"] Associations to include with the assignment.
# @argument search_term (optional) The partial title of the assignments to match and return.
# @argument override_assignment_dates [Optional, Boolean] # @argument override_assignment_dates [Optional, Boolean]
# Apply assignment overrides for each assignment, defaults to true. # Apply assignment overrides for each assignment, defaults to true.
# @returns [Assignment] # @returns [Assignment]
@ -300,6 +301,8 @@ class AssignmentsApiController < ApplicationController
includes(:assignment_group, :rubric_association, :rubric). includes(:assignment_group, :rubric_association, :rubric).
reorder("assignment_groups.position, assignments.position") reorder("assignment_groups.position, assignments.position")
@assignments = Assignment.search_by_attribute(@assignments, :title, params[:search_term])
#fake assignment used for checking if the @current_user can read unpublished assignments #fake assignment used for checking if the @current_user can read unpublished assignments
fake = @context.assignments.new fake = @context.assignments.new
fake.workflow_state = 'unpublished' fake.workflow_state = 'unpublished'

View File

@ -156,6 +156,7 @@ class DiscussionTopicsController < ApplicationController
# @argument order_by Determines the order of the discussion topic list. May be one of "position", or "recent_activity". Defaults to "position". # @argument order_by Determines the order of the discussion topic list. May be one of "position", or "recent_activity". Defaults to "position".
# @argument scope [Optional, "locked"|"unlocked"] Only return discussion topics in the given state. Defaults to including locked and unlocked topics. Filtering is done after pagination, so pages may be smaller than requested if topics are filtered # @argument scope [Optional, "locked"|"unlocked"] Only return discussion topics in the given state. Defaults to including locked and unlocked topics. Filtering is done after pagination, so pages may be smaller than requested if topics are filtered
# @argument only_announcements [Optional] Boolean, return announcements instead of discussion topics. Defaults to false # @argument only_announcements [Optional] Boolean, return announcements instead of discussion topics. Defaults to false
# @argument search_term (optional) The partial title of the discussion topics to match and return.
# #
# @example_request # @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \ # curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
@ -174,6 +175,8 @@ class DiscussionTopicsController < ApplicationController
scope = params[:order_by] == 'recent_activity' ? scope.by_last_reply_at : scope.by_position scope = params[:order_by] == 'recent_activity' ? scope.by_last_reply_at : scope.by_position
scope = DiscussionTopic.search_by_attribute(scope, :title, params[:search_term])
@topics = Api.paginate(scope, self, topic_pagination_url) @topics = Api.paginate(scope, self, topic_pagination_url)
@topics.reject! { |t| t.locked? || t.locked_for?(@current_user) } if params[:scope] == 'unlocked' @topics.reject! { |t| t.locked? || t.locked_for?(@current_user) } if params[:scope] == 'unlocked'
@topics.select! { |t| t.locked? || t.locked_for?(@current_user) } if params[:scope] == 'locked' @topics.select! { |t| t.locked? || t.locked_for?(@current_user) } if params[:scope] == 'locked'

View File

@ -31,6 +31,7 @@ class ExternalToolsController < ApplicationController
# Returns the paginated list of external tools for the current context. # Returns the paginated list of external tools for the current context.
# See the get request docs for a single tool for a list of properties on an external tool. # See the get request docs for a single tool for a list of properties on an external tool.
# #
# @argument search_term (optional) The partial name of the tools to match and return.
# #
# @example_response # @example_response
# [ # [
@ -70,6 +71,7 @@ class ExternalToolsController < ApplicationController
else else
@tools = @context.context_external_tools.active @tools = @context.context_external_tools.active
end end
@tools = ContextExternalTool.search_by_attribute(@tools, :name, params[:search_term])
respond_to do |format| respond_to do |format|
@tools = Api.paginate(@tools, self, tool_pagination_url) @tools = Api.paginate(@tools, self, tool_pagination_url)
format.json {render :json => external_tools_json(@tools, @context, @current_user, session)} format.json {render :json => external_tools_json(@tools, @context, @current_user, session)}

View File

@ -133,6 +133,8 @@ class QuizzesApiController < ApplicationController
# #
# Returns the list of Quizzes in this course. # Returns the list of Quizzes in this course.
# #
# @argument search_term (optional) The partial title of the quizzes to match and return.
#
# @example_request # @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/quizzes \ # curl https://<canvas>/api/v1/courses/<course_id>/quizzes \
# -H 'Authorization: Bearer <token>' # -H 'Authorization: Bearer <token>'
@ -141,7 +143,8 @@ class QuizzesApiController < ApplicationController
def index def index
if authorized_action(@context, @current_user, :read) && tab_enabled?(@context.class::TAB_QUIZZES) if authorized_action(@context, @current_user, :read) && tab_enabled?(@context.class::TAB_QUIZZES)
api_route = polymorphic_url([:api, :v1, @context, :quizzes]) api_route = polymorphic_url([:api, :v1, @context, :quizzes])
@quizzes = Api.paginate(@context.quizzes.active, self, api_route) scope = Quiz.search_by_attribute(@context.quizzes.active, :title, params[:search_term])
@quizzes = Api.paginate(scope, self, api_route)
render :json => quizzes_json(@quizzes, @context, @current_user, session) render :json => quizzes_json(@quizzes, @context, @current_user, session)
end end
end end

View File

@ -26,6 +26,7 @@ class Assignment < ActiveRecord::Base
include Mutable include Mutable
include ContextModuleItem include ContextModuleItem
include DatesOverridable include DatesOverridable
include SearchTermHelper
attr_accessible :title, :name, :description, :due_at, :points_possible, attr_accessible :title, :name, :description, :due_at, :points_possible,
:min_score, :max_score, :mastery_score, :grading_type, :submission_types, :min_score, :max_score, :mastery_score, :grading_type, :submission_types,

View File

@ -1,5 +1,7 @@
class ContextExternalTool < ActiveRecord::Base class ContextExternalTool < ActiveRecord::Base
include Workflow include Workflow
include SearchTermHelper
has_many :content_tags, :as => :content has_many :content_tags, :as => :content
belongs_to :context, :polymorphic => true belongs_to :context, :polymorphic => true
belongs_to :cloned_item belongs_to :cloned_item

View File

@ -24,6 +24,7 @@ class DiscussionTopic < ActiveRecord::Base
include CopyAuthorizedLinks include CopyAuthorizedLinks
include TextHelper include TextHelper
include ContextModuleItem include ContextModuleItem
include SearchTermHelper
attr_accessible :title, :message, :user, :delayed_post_at, :lock_at, :assignment, attr_accessible :title, :message, :user, :delayed_post_at, :lock_at, :assignment,
:plaintext_message, :podcast_enabled, :podcast_has_student_posts, :plaintext_message, :podcast_enabled, :podcast_has_student_posts,

View File

@ -26,6 +26,7 @@ class Quiz < ActiveRecord::Base
extend ActionView::Helpers::SanitizeHelper::ClassMethods extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ContextModuleItem include ContextModuleItem
include DatesOverridable include DatesOverridable
include SearchTermHelper
attr_accessible :title, :description, :points_possible, :assignment_id, :shuffle_answers, attr_accessible :title, :description, :points_possible, :assignment_id, :shuffle_answers,
:show_correct_answers, :time_limit, :allowed_attempts, :scoring_policy, :quiz_type, :show_correct_answers, :time_limit, :allowed_attempts, :scoring_policy, :quiz_type,

View File

@ -0,0 +1,33 @@
class AddGistIndexesForApiSearch < ActiveRecord::Migration
self.transactional = false
tag :predeploy
def self.up
if is_postgres?
connection.transaction(:requires_new => true) do
begin
execute('create extension if not exists pg_trgm;')
rescue ActiveRecord::StatementInvalid
raise ActiveRecord::Rollback
end
end
if has_postgres_proc?('show_trgm')
concurrently = " CONCURRENTLY" if connection.open_transactions == 0
execute("create index#{concurrently} index_trgm_context_external_tools_name on context_external_tools USING gist(lower(name) gist_trgm_ops);")
execute("create index#{concurrently} index_trgm_assignments_title on assignments USING gist(lower(title) gist_trgm_ops);")
execute("create index#{concurrently} index_trgm_quizzes_title on quizzes USING gist(lower(title) gist_trgm_ops);")
execute("create index#{concurrently} index_trgm_discussion_topics_title on discussion_topics USING gist(lower(title) gist_trgm_ops);")
end
end
end
def self.down
if is_postgres?
execute('drop index if exists index_trgm_context_external_tools_name;')
execute('drop index if exists index_trgm_assignments_title;')
execute('drop index if exists index_trgm_quizzes_title;')
execute('drop index if exists index_trgm_discussion_topics_title;')
end
end
end

View File

@ -116,6 +116,24 @@ describe AssignmentsApiController, :type => :integration do
assignment4) assignment4)
end end
it "should search for assignments by title" do
course_with_teacher(:active_all => true)
2.times {|i| @course.assignments.create!(:title => "first_#{i}") }
ids = @course.assignments.map(&:id)
2.times {|i| @course.assignments.create!(:title => "second_#{i}") }
json = api_call(:get,
"/api/v1/courses/#{@course.id}/assignments.json?search_term=fir",
{
:controller => 'assignments_api',
:action => 'index',
:format => 'json',
:course_id => @course.id.to_s,
:search_term => 'fir'
})
json.map{|h| h['id']}.sort.should == ids.sort
end
it "should return the assignments list with API-formatted Rubric data" do it "should return the assignments list with API-formatted Rubric data" do
# the API changes the structure of the data quite a bit, to hide # the API changes the structure of the data quite a bit, to hide
# implementation details and ease API use. # implementation details and ease API use.

View File

@ -303,6 +303,17 @@ describe DiscussionTopicsController, :type => :integration do
json.last.should == @response_json.merge("subscribed" => @sub.subscribed?(@user)) json.last.should == @response_json.merge("subscribed" => @sub.subscribed?(@user))
end end
it "should search discussion topics by title" do
ids = @course.discussion_topics.map(&:id)
create_topic(@course, :title => "ignore me", :message => "<p>i'm subversive</p>")
create_topic(@course, :title => "ignore me2", :message => "<p>i'm subversive</p>")
json = api_call(:get, "/api/v1/courses/#{@course.id}/discussion_topics.json?search_term=topic",
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :course_id => @course.id.to_s,
:search_term => 'topic'})
json.map{|h| h['id']}.sort.should == ids.sort
end
it "should order topics by descending position by default" do it "should order topics by descending position by default" do
@topic2 = create_topic(@course, :title => "Topic 2", :message => "<p>content here</p>") @topic2 = create_topic(@course, :title => "Topic 2", :message => "<p>content here</p>")
@topic3 = create_topic(@course, :title => "Topic 3", :message => "<p>content here</p>") @topic3 = create_topic(@course, :title => "Topic 3", :message => "<p>content here</p>")

View File

@ -37,6 +37,10 @@ describe ExternalToolsController, :type => :integration do
index_call(@course) index_call(@course)
end end
it "should search for external tools by name" do
search_call(@course)
end
it "should create an external tool" do it "should create an external tool" do
create_call(@course) create_call(@course)
end end
@ -87,6 +91,10 @@ describe ExternalToolsController, :type => :integration do
index_call(@account, "account") index_call(@account, "account")
end end
it "should search for external tools by name" do
search_call(@account, "account")
end
it "should create an external tool" do it "should create an external tool" do
create_call(@account, "account") create_call(@account, "account")
end end
@ -144,6 +152,19 @@ describe ExternalToolsController, :type => :integration do
json.first.diff(example_json(et)).should == {} json.first.diff(example_json(et)).should == {}
end end
def search_call(context, type="course")
2.times { |i| context.context_external_tools.create!(:name => "first_#{i}", :consumer_key => "fakefake", :shared_secret => "sofakefake", :url => "http://www.example.com/ims/lti") }
ids = context.context_external_tools.map(&:id)
2.times { |i| context.context_external_tools.create!(:name => "second_#{i}", :consumer_key => "fakefake", :shared_secret => "sofakefake", :url => "http://www.example.com/ims/lti") }
json = api_call(:get, "/api/v1/#{type}s/#{context.id}/external_tools.json?search_term=fir",
{:controller => 'external_tools', :action => 'index', :format => 'json',
:"#{type}_id" => context.id.to_s, :search_term => 'fir'})
json.map{|h| h['id']}.sort.should == ids.sort
end
def create_call(context, type="course") def create_call(context, type="course")
json = api_call(:post, "/api/v1/#{type}s/#{context.id}/external_tools.json", json = api_call(:post, "/api/v1/#{type}s/#{context.id}/external_tools.json",
{:controller => 'external_tools', :action => 'create', :format => 'json', {:controller => 'external_tools', :action => 'create', :format => 'json',

View File

@ -51,6 +51,18 @@ describe QuizzesApiController, :type => :integration do
quiz_ids.should == quizzes.map(&:id) quiz_ids.should == quizzes.map(&:id)
end end
it "should search for quizzes by title" do
2.times{ |i| @course.quizzes.create! :title => "first_#{i}" }
ids = @course.quizzes.map(&:id)
2.times{ |i| @course.quizzes.create! :title => "second_#{i}" }
json = api_call(:get, "/api/v1/courses/#{@course.id}/quizzes?search_term=fir",
:controller=>"quizzes_api", :action=>"index", :format=>"json", :course_id=>"#{@course.id}",
:search_term => 'fir')
json.map{|h| h['id'] }.sort.should == ids.sort
end
it "should return unauthorized if the quiz tab is disabled" do it "should return unauthorized if the quiz tab is disabled" do
@course.tab_configuration = [ { :id => Course::TAB_QUIZZES, :hidden => true } ] @course.tab_configuration = [ { :id => Course::TAB_QUIZZES, :hidden => true } ]
student_in_course(:active_all => true, :course => @course) student_in_course(:active_all => true, :course => @course)