Skip to content

Commit f31d2bd

Browse files
committed
add search_term to files, modules, and module item apis
test plan: * test the new /courses/:course_id/files index endpoint, which should return all files that belong to a course * refer to the API documentation to test the search_term parameter (which should work as with other API index actions), for: - Files (both folder and course specific) - Modules - Module Items closes #CNVS-6904 Change-Id: I4c6f80792cda453d53bf48741d14e851f5040dd4 Reviewed-on: https://gerrit.instructure.com/22746 Reviewed-by: Jeremy Stanley <jeremy@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: August Thornton <august@instructure.com> Product-Review: Bracken Mosbacker <bracken@instructure.com>
1 parent 66c4aa1 commit f31d2bd

15 files changed

Lines changed: 260 additions & 16 deletions

app/controllers/context_module_items_api_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class ContextModuleItemsApiController < ApplicationController
9090
#
9191
# @argument include[] ["content_details"] If included, will return additional details specific to the content associated with each item.
9292
# Refer to the {api:Modules:Module%20Item Module Item specification} for more details.
93+
# @argument search_term (optional) The partial title of the items to match and return.
9394
#
9495
# @example_request
9596
# curl -H 'Authorization: Bearer <token>' \
@@ -102,6 +103,7 @@ def index
102103
ContextModule.send(:preload_associations, mod, {:content_tags => :content})
103104
route = polymorphic_url([:api_v1, @context, mod, :items])
104105
scope = mod.content_tags_visible_to(@current_user)
106+
scope = ContentTag.search_by_attribute(scope, :title, params[:search_term])
105107
items = Api.paginate(scope, self, route)
106108
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
107109
render :json => items.map { |item| module_item_json(item, @current_user, session, mod, prog, Array(params[:include])) }

app/controllers/context_modules_api_controller.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class ContextModulesApiController < ApplicationController
8585
# if items are not returned.
8686
# @argument include[] ["content_details"] (Requires include['items']) Returns additional details with module items specific to their associated content items.
8787
# Refer to the {api:Modules:Module%20Item Module Item specification} for more details.
88+
# @argument search_term (optional) The partial name of the modules (and module items, if include['items'] is specified) to match and return.
8889
#
8990
# @example_request
9091
# curl -H 'Authorization: Bearer <token>' \
@@ -95,16 +96,24 @@ def index
9596
if authorized_action(@context, @current_user, :read)
9697
route = polymorphic_url([:api_v1, @context, :context_modules])
9798
scope = @context.modules_visible_to(@current_user)
98-
modules = Api.paginate(scope, self, route)
99+
99100
includes = Array(params[:include])
101+
scope = ContextModule.search_by_attribute(scope, :name, params[:search_term]) unless includes.include?('items')
102+
modules = Api.paginate(scope, self, route)
103+
100104
ContextModule.send(:preload_associations, modules, {:content_tags => :content}) if includes.include?('items')
101105

102106
modules_and_progressions = if @context.grants_right?(@current_user, session, :participate_as_student)
103107
modules.map { |m| [m, m.evaluate_for(@current_user, true)] }
104108
else
105109
modules.map { |m| [m, nil] }
106110
end
107-
render :json => modules_and_progressions.map { |mod, prog| module_json(mod, @current_user, session, prog, includes) }
111+
opts = {}
112+
if includes.include?('items') && params[:search_term].present?
113+
SearchTermHelper.validate_search_term(params[:search_term])
114+
opts[:search_term] = params[:search_term]
115+
end
116+
render :json => modules_and_progressions.map { |mod, prog| module_json(mod, @current_user, session, prog, includes, opts) }.compact
108117
end
109118
end
110119

app/controllers/files_controller.rb

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ def index
116116
end
117117

118118
# @API List files
119-
# Returns the paginated list of files for the folder.
119+
# Returns the paginated list of files for the folder or course.
120120
#
121121
# @argument content_types[] [optional] Filter results by content-type. You can specify type/subtype pairs (e.g., 'image/jpeg'), or simply types (e.g., 'image', which will match 'image/gif', 'image/jpeg', etc.).
122+
# @argument search_term (optional) The partial name of the files to match and return.
122123
#
123124
# @example_request
124125
#
@@ -127,16 +128,34 @@ def index
127128
#
128129
# @returns [File]
129130
def api_index
130-
folder = Folder.find(params[:id])
131+
get_context
132+
if @context
133+
folder = Folder.root_folders(@context).first
134+
raise ActiveRecord::RecordNotFound unless folder
135+
context_index = true
136+
else
137+
folder = Folder.find(params[:id])
138+
end
139+
131140
if authorized_action(folder, @current_user, :read_contents)
132-
@context = folder.context
141+
@context = folder.context unless context_index
133142
can_manage_files = @context.grants_right?(@current_user, session, :manage_files)
134143

135-
if can_manage_files
136-
scope = folder.active_file_attachments
144+
if context_index
145+
if can_manage_files
146+
scope = @context.attachments.not_deleted
147+
else
148+
scope = @context.attachments.visible.not_hidden.not_locked.where(
149+
:folder_id => @context.active_folders.not_hidden.not_locked)
150+
end
137151
else
138-
scope = folder.visible_file_attachments.not_hidden.not_locked
152+
if can_manage_files
153+
scope = folder.active_file_attachments
154+
else
155+
scope = folder.visible_file_attachments.not_hidden.not_locked
156+
end
139157
end
158+
scope = Attachment.search_by_attribute(scope, :display_name, params[:search_term])
140159
if params[:sort_by] == 'position'
141160
scope = scope.by_position_then_display_name
142161
else
@@ -147,7 +166,8 @@ def api_index
147166
scope = scope.by_content_types(Array(params[:content_types]))
148167
end
149168

150-
@files = Api.paginate(scope, self, api_v1_list_files_url(@folder))
169+
url = context_index ? context_files_url : api_v1_list_files_url(folder)
170+
@files = Api.paginate(scope, self, url)
151171
render :json => attachments_json(@files, @current_user, {}, :can_manage_files => can_manage_files)
152172
end
153173
end

app/models/attachment.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def self.display_name_order_by_clause(table = nil)
2525
attr_accessible :context, :folder, :filename, :display_name, :user, :locked, :position, :lock_at, :unlock_at, :uploaded_data, :hidden
2626
include HasContentTags
2727
include ContextModuleItem
28+
include SearchTermHelper
2829

2930
attr_accessor :podcast_associated_asset, :submission_attachment
3031

@@ -1277,6 +1278,7 @@ def self.filtering_scribd_submits?
12771278
state :unattached_temporary
12781279
end
12791280

1281+
scope :visible, where(['attachments.file_state in (?, ?)', 'available', 'public'])
12801282
scope :not_deleted, where("attachments.file_state<>'deleted'")
12811283

12821284
scope :not_hidden, where("attachments.file_state<>'hidden'")

app/models/content_tag.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def initialize( alignment )
2525
end
2626
end
2727
include Workflow
28+
include SearchTermHelper
2829
belongs_to :content, :polymorphic => true
2930
belongs_to :context, :polymorphic => true
3031
belongs_to :associated_asset, :polymorphic => true

app/models/context_module.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
class ContextModule < ActiveRecord::Base
2020
include Workflow
21+
include SearchTermHelper
2122
attr_accessible :context, :name, :unlock_at, :require_sequential_progress, :completion_requirements, :prerequisites
2223
belongs_to :context, :polymorphic => true
2324
belongs_to :cloned_item

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@
837837
post 'courses/:course_id/preview_html', :action => :preview_html
838838
post 'courses/:course_id/course_copy', :controller => :content_imports, :action => :copy_course_content
839839
get 'courses/:course_id/course_copy/:id', :controller => :content_imports, :action => :copy_course_status, :path_name => :course_copy_status
840+
get 'courses/:course_id/files', :controller => :files, :action => :api_index, :path_name => 'course_files'
840841
post 'courses/:course_id/files', :action => :create_file, :path_name => 'course_create_file'
841842
post 'courses/:course_id/folders', :controller => :folders, :action => :create
842843
get 'courses/:course_id/folders/:id', :controller => :folders, :action => :show, :path_name => 'course_folder'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class AddAdditionalGistIndexesForApiSearch < ActiveRecord::Migration
2+
self.transactional = false
3+
tag :predeploy
4+
5+
def self.up
6+
if is_postgres?
7+
connection.transaction(:requires_new => true) do
8+
begin
9+
execute('create extension if not exists pg_trgm;')
10+
rescue ActiveRecord::StatementInvalid
11+
raise ActiveRecord::Rollback
12+
end
13+
end
14+
15+
if has_postgres_proc?('show_trgm')
16+
concurrently = " CONCURRENTLY" if connection.open_transactions == 0
17+
execute("create index#{concurrently} index_trgm_attachments_display_name on attachments USING gist(lower(display_name) gist_trgm_ops);")
18+
execute("create index#{concurrently} index_trgm_context_modules_name on context_modules USING gist(lower(name) gist_trgm_ops);")
19+
execute("create index#{concurrently} index_trgm_content_tags_title on content_tags USING gist(lower(title) gist_trgm_ops);")
20+
end
21+
end
22+
end
23+
24+
def self.down
25+
if is_postgres?
26+
execute('drop index if exists index_trgm_attachments_display_name;')
27+
execute('drop index if exists index_trgm_context_modules_name;')
28+
execute('drop index if exists index_trgm_content_tags_title;')
29+
end
30+
end
31+
end

lib/api/v1/attachment.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,9 @@ def process_attachment_params(params)
173173
new_atts[:hidden] = value_to_boolean(params[:hidden]) if params.has_key?(:hidden)
174174
new_atts
175175
end
176+
177+
def context_files_url
178+
# change if context_api_index route is expanded to other contexts besides courses
179+
api_v1_course_files_url(@context)
180+
end
176181
end

lib/api/v1/context_module.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module Api::V1::ContextModule
2525
MODULE_ITEM_JSON_ATTRS = %w(id position title indent)
2626

2727
# optionally pass progression to include 'state', 'completed_at'
28-
def module_json(context_module, current_user, session, progression = nil, includes = [])
28+
def module_json(context_module, current_user, session, progression = nil, includes = [], opts = {})
2929
hash = api_json(context_module, current_user, session, :only => MODULE_JSON_ATTRS)
3030
hash['require_sequential_progress'] = !!context_module.require_sequential_progress
3131
hash['prerequisite_module_ids'] = context_module.prerequisites.reject{|p| p[:type] != 'context_module'}.map{|p| p[:id]}
@@ -40,6 +40,10 @@ def module_json(context_module, current_user, session, progression = nil, includ
4040
hash['items_count'] = count
4141
hash['items_url'] = polymorphic_url([:api_v1, context_module.context, context_module, :items])
4242
if includes.include?('items') && count <= Setting.get_cached('api_max_per_page', '50').to_i
43+
if opts[:search_term].present? && !context_module.matches_attribute?(:name, opts[:search_term])
44+
tags = ContentTag.search_by_attribute(tags, :title, opts[:search_term])
45+
return nil if tags.count == 0
46+
end
4347
item_includes = includes & ['content_details']
4448
hash['items'] = tags.map do |tag|
4549
module_item_json(tag, current_user, session, context_module, progression, item_includes, :has_update_rights => has_update_rights)

0 commit comments

Comments
 (0)