Add group files to the rce

closes: LS-1392
flag=rce_enhancements

test plan:
  prereqs:
  1) if you plan on testing group media, you'll need g/247879 built into
        your RCS when testing this.
  2)rce in a group in a course
      - /course/:id/groups
      - +Group Set
      - +Group
      - add student(s)
      - from the group's kabob menu, select "Visit Group Home Page"
      - +Announcement
      - +Announcement again
      - TADA, you're in an RCE in a group context

  - on the toolbar, Images > Upload Image and upload an image
  - on the toolbar, Images > Group Images
  > expect the image you just uploaded to be listed
  - click it
  > expect it to be embedded in the RCE
  - repeat for Documents
  - repeat for Media

Change-Id: I686fe4c42df32cb7e767fe2e277530cb472b2fd6
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/247738
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
Product-Review: Peyton Craighill <pcraighill@instructure.com>
This commit is contained in:
Ed Schiebel 2020-09-15 13:26:52 -04:00
parent b1677c838f
commit a204b384dc
10 changed files with 167 additions and 36 deletions

View File

@ -71,8 +71,8 @@
class MediaObjectsController < ApplicationController
include Api::V1::MediaObject
before_action :load_media_object, :except => [:index, :update_media_object]
before_action :require_user, :except => [:show, :iframe_media_player]
before_action :load_media_object, except: %i[index update_media_object]
before_action :require_user, except: %i[show iframe_media_player]
# @{not an}API Show Media Object Details
# This isn't an API because it needs to work for non-logged in users (video in public course)
@ -85,7 +85,7 @@ class MediaObjectsController < ApplicationController
#
# @returns MediaObject
def show
render :json => media_object_api_json(@media_object, @current_user, session)
render json: media_object_api_json(@media_object, @current_user, session)
end
# @API List Media Objects
@ -121,31 +121,41 @@ class MediaObjectsController < ApplicationController
# @returns [MediaObject]
def index
if params[:course_id]
course = Course.find(params[:course_id])
root_folder = Folder.root_folders(course).first
context = Course.find(params[:course_id])
url = api_v1_course_media_objects_url
elsif params[:group_id]
context = Group.find(params[:group_id])
url = api_v1_group_media_objects_url
end
if context
root_folder = Folder.root_folders(context).first
if root_folder.grants_right?(@current_user, :read_contents)
# if the user has access to the course's root folder, let's
# assume they have access to the course's media, even if it's
# if the user has access to the context's root folder, let's
# assume they have access to the context's media, even if it's
# media not associated with an Attachment in there
scope = MediaObject.active.where(:context => course)
url = api_v1_course_media_objects_url
scope = MediaObject.active.where(context: context)
else
return render_unauthorized_action # not allowed to view files in the course
return render_unauthorized_action # not allowed to view files in the context
end
else
scope = MediaObject.active.where(context: @current_user)
url = api_v1_media_objects_url
end
order_dir = params[:order] == "desc" ? "desc" : "asc"
order_by = params[:sort] || "title"
order_by = MediaObject.best_unicode_collation_key('COALESCE(user_entered_title, title)') if order_by == "title"
order_dir = params[:order] == 'desc' ? 'desc' : 'asc'
order_by = params[:sort] || 'title'
if order_by == 'title'
order_by = MediaObject.best_unicode_collation_key('COALESCE(user_entered_title, title)')
end
scope = scope.order(order_by => order_dir)
exclude = params[:exclude] || []
media_objects = Api.paginate(scope, self, url).
map{ |mo| media_object_api_json(mo, @current_user, session, exclude)}
render :json => media_objects
media_objects =
Api.paginate(scope, self, url).map do |mo|
media_object_api_json(mo, @current_user, session, exclude)
end
render json: media_objects
end
# @API Update Media Object
@ -153,20 +163,28 @@ class MediaObjectsController < ApplicationController
# @argument user_entered_title [String] The new title.
#
def update_media_object
# media objects don't have any permissions associated with them,
# so we just check that this is the user's media
if params[:media_object_id]
@media_object = MediaObject.by_media_id(params[:media_object_id]).first
return render_unauthorized_action unless @media_object
return render_unauthorized_action unless @current_user&.id
# media objects don't have any permissions associated with them,
# so we just check that this is the user's media
return render_unauthorized_action unless @media_object.user_id == @current_user.id
return render json: {message: "The user_entered_title parameter must have a value"}, status: :bad_request if params[:user_entered_title].blank?
if params[:user_entered_title].blank?
return(
render json: { message: 'The user_entered_title parameter must have a value' },
status: :bad_request
)
end
self.extend TextHelper
@media_object.user_entered_title = CanvasTextHelper.truncate_text(params[:user_entered_title], :max_length => 255)
@media_object.user_entered_title =
CanvasTextHelper.truncate_text(params[:user_entered_title], max_length: 255)
@media_object.save!
render :json => media_object_api_json(@media_object, @current_user, session, ["sources", "tracks"])
render json: media_object_api_json(@media_object, @current_user, session, %w[sources tracks])
end
end
@ -177,7 +195,8 @@ class MediaObjectsController < ApplicationController
js_env media_object: media_object_api_json(@media_object, @current_user, session)
js_bundle :media_player_iframe_content
css_bundle :media_player
render html: "<div id='player_container'>#{I18n.t('Loading...')}</div>".html_safe, layout: 'layouts/bare'
render html: "<div id='player_container'>#{I18n.t('Loading...')}</div>".html_safe,
layout: 'layouts/bare'
end
private
@ -188,10 +207,11 @@ class MediaObjectsController < ApplicationController
# Unfortunately, we don't have media_object entities created for everything,
# so we use this opportunity to create the object if it does not exist.
@media_object = MediaObject.create_if_id_exists(params[:media_object_id])
@media_object.send_later_enqueue_args(:retrieve_details, {
:singleton => "retrieve_media_details:#{@media_object.media_id}"
})
increment_request_cost(Setting.get("missed_media_additional_request_cost", "200").to_i)
@media_object.send_later_enqueue_args(
:retrieve_details,
{ singleton: "retrieve_media_details:#{@media_object.media_id}" }
)
increment_request_cost(Setting.get('missed_media_additional_request_cost', '200').to_i)
end
@media_object.viewed!

View File

@ -1011,6 +1011,7 @@ CanvasRails::Application.routes.draw do
get 'courses/:course_id/folders/:id', controller: :folders, action: :show, as: 'course_folder'
get 'media_objects', controller: 'media_objects', action: :index, as: :media_objects
get 'courses/:course_id/media_objects', controller: 'media_objects', action: :index, as: :course_media_objects
get 'groups/:group_id/media_objects', controller: 'media_objects', action: :index, as: :group_media_objects
put 'accounts/:account_id/courses', action: :batch_update
post 'courses/:course_id/ping', action: :ping, as: 'course_ping'

View File

@ -23,6 +23,7 @@ import {isOKToLink} from '../../contentInsertionUtils'
const COURSE_PLUGIN_KEY = 'course_documents'
const USER_PLUGIN_KEY = 'user_documents'
const GROUP_PLUGIN_KEY = 'group_documents'
function getMenuItems(ed) {
const contextType = ed.settings.canvas_rce_user_context.type
@ -37,6 +38,11 @@ function getMenuItems(ed) {
text: formatMessage('Course Documents'),
value: 'instructure_course_document'
})
} else if (contextType === 'group') {
items.push({
text: formatMessage('Group Documents'),
value: 'instructure_group_document'
})
}
items.push({
text: formatMessage('User Documents'),
@ -58,6 +64,10 @@ function doMenuItem(ed, value) {
ed.focus(true)
ed.execCommand('instructureTrayForDocuments', false, USER_PLUGIN_KEY)
break
case 'instructure_group_document':
ed.focus(true)
ed.execCommand('instructureTrayForDocuments', false, GROUP_PLUGIN_KEY)
break
}
}

View File

@ -25,6 +25,7 @@ import clickCallback from './clickCallback'
const COURSE_PLUGIN_KEY = 'course_images'
const USER_PLUGIN_KEY = 'user_images'
const GROUP_PLUGIN_KEY = 'group_images'
const trayController = new TrayController()
@ -41,6 +42,11 @@ function getMenuItems(ed) {
text: formatMessage('Course Images'),
value: 'instructure_course_image'
})
} else if (contextType === 'group') {
items.push({
text: formatMessage('Group Images'),
value: 'instructure_group_image'
})
}
items.push({
text: formatMessage('User Images'),
@ -58,6 +64,10 @@ function doMenuItem(ed, value) {
ed.focus(true)
ed.execCommand('instructureTrayForImages', false, COURSE_PLUGIN_KEY)
break
case 'instructure_group_image':
ed.focus(true)
ed.execCommand('instructureTrayForImages', false, GROUP_PLUGIN_KEY)
break
case 'instructure_user_image':
ed.focus(true)
ed.execCommand('instructureTrayForImages', false, USER_PLUGIN_KEY)

View File

@ -27,6 +27,7 @@ const trayController = new TrayController()
const COURSE_PLUGIN_KEY = 'course_media'
const USER_PLUGIN_KEY = 'user_media'
const GROUP_PLUGIN_KEY = 'group_media'
function getMenuItems(ed) {
const contextType = ed.settings.canvas_rce_user_context.type
@ -43,6 +44,11 @@ function getMenuItems(ed) {
text: formatMessage('Course Media'),
value: 'instructure_course_media'
})
} else if (contextType === 'group') {
items.push({
text: formatMessage('Group Media'),
value: 'instructure_group_media'
})
}
items.push({
text: formatMessage('User Media'),
@ -60,6 +66,10 @@ function doMenuItem(ed, value) {
ed.focus(true)
ed.execCommand('instructureTrayForMedia', false, COURSE_PLUGIN_KEY)
break
case 'instructure_group_media':
ed.focus(true)
ed.execCommand('instructureTrayForMedia', false, GROUP_PLUGIN_KEY)
break
case 'instructure_user_media':
ed.focus(true)
ed.execCommand('instructureTrayForMedia', false, USER_PLUGIN_KEY)

View File

@ -46,17 +46,17 @@ function getTrayLabel(contentType, contentSubtype, contextType) {
switch (contentSubtype) {
case 'images':
return contentType === 'course_files'
? formatMessage('Course Images')
: formatMessage('User Images')
if (contentType === 'course_files') return formatMessage('Course Images')
if (contentType === 'group_files') return formatMessage('Group Images')
return formatMessage('User Images')
case 'media':
return contentType === 'course_files'
? formatMessage('Course Media')
: formatMessage('User Media')
if (contentType === 'course_files') return formatMessage('Course Media')
if (contentType === 'group_files') return formatMessage('Group Media')
return formatMessage('User Media')
case 'documents':
return contentType === 'course_files'
? formatMessage('Course Documents')
: formatMessage('User Documents')
if (contentType === 'course_files') return formatMessage('Course Documents')
if (contentType === 'group_files') return formatMessage('Group Documents')
return formatMessage('User Documents')
default:
return formatMessage('Tray') // Shouldn't ever get here
}
@ -107,6 +107,13 @@ const FILTER_SETTINGS_BY_PLUGIN = {
sortValue: 'date_added',
sortDir: 'desc'
},
group_documents: {
contextType: 'group',
contentType: 'group_files',
contentSubtype: 'documents',
sortValue: 'date_added',
sortDir: 'desc'
},
user_images: {
contextType: 'user',
contentType: 'user_files',
@ -121,6 +128,13 @@ const FILTER_SETTINGS_BY_PLUGIN = {
sortValue: 'date_added',
sortDir: 'desc'
},
group_images: {
contextType: 'group',
contentType: 'group_files',
contentSubtype: 'images',
sortValue: 'date_added',
sortDir: 'desc'
},
user_media: {
contextType: 'user',
contentType: 'user_files',
@ -135,6 +149,13 @@ const FILTER_SETTINGS_BY_PLUGIN = {
sortValue: 'date_added',
sortDir: 'desc'
},
group_media: {
contextType: 'group',
contentType: 'group_files',
contentSubtype: 'media',
sortValue: 'date_added',
sortDir: 'desc'
},
course_links: {
contextType: 'course',
contentType: 'links',
@ -225,6 +246,10 @@ export default function CanvasContentTray(props) {
contextType = 'user'
contextId = props.containingContext.userId
break
case 'group_files':
contextType = 'group'
contextId = props.containingContext.contextId
break
case 'course_files':
case 'links':
contextType = props.contextType

View File

@ -73,6 +73,13 @@ function renderTypeOptions(contentType, contentSubtype, userContextType) {
</option>
)
}
if (userContextType === 'group' && contentType !== 'links' && contentSubtype !== 'all') {
options.push(
<option key="group_files" value="group_files" icon={IconFolderLine}>
{fileLabelFromContext('group')}
</option>
)
}
options.push(
<option key="user_files" value="user_files" icon={IconFolderLine}>
{fileLabelFromContext(contentType === 'links' || contentSubtype === 'all' ? 'files' : 'user')}
@ -182,7 +189,7 @@ Filter.propTypes = {
/**
* `contentType` is the primary filter setting (e.g. links, files)
*/
contentType: oneOf(['links', 'user_files', 'course_files']).isRequired,
contentType: oneOf(['links', 'user_files', 'course_files', 'group_files']).isRequired,
/**
* `onChange` is called when any of the Filter settings are changed

View File

@ -120,6 +120,21 @@ describe('RCE Plugins > CanvasContentTray', () => {
await showTrayForPlugin('links')
expect(getTrayLabel()).toEqual('Group Links')
})
it('is labeled with "Group Images" when using the "images" content type', async () => {
await showTrayForPlugin('group_images')
expect(getTrayLabel()).toEqual('Group Images')
})
it('is labeled with "Group Media" when using the "media" content type', async () => {
await showTrayForPlugin('group_media')
expect(getTrayLabel()).toEqual('Group Media')
})
it('is labeled with "Group Documents" when using the "group_documents" content type', async () => {
await showTrayForPlugin('group_documents')
expect(getTrayLabel()).toEqual('Group Documents')
})
})
describe('content panel', () => {

View File

@ -134,6 +134,14 @@ describe('RCE Plugins > Filter', () => {
expect(component.getByLabelText('Content Type').value).toEqual('Course Files')
})
it('has "Group" options', () => {
renderComponent({userContextType: 'group'})
selectContentType('Group Files')
expect(currentFilterSettings.contentType).toEqual('group_files')
expect(component.getByLabelText('Content Type').value).toEqual('Group Files')
})
it('has "User" options', () => {
renderComponent({userContextType: 'course'})

View File

@ -414,6 +414,31 @@ describe MediaObjectsController do
])
end
it "will limit return to group media" do
course_with_teacher_logged_in(active_all: true)
gcat = @course.group_categories.create!(:name => "My Group Category")
@group = Group.create!(:name => "some group", :group_category => gcat, :context => @course)
mo1 = MediaObject.create!(:user_id => @user, :context => @group, :media_id => "in_group")
MediaObject.create!(:user_id => @user, :context => @course, :media_id => "in_course_with_att")
@course.attachments.create!(:media_entry_id => "in_course_with_att", :uploaded_data => stub_png_data)
MediaObject.create!(:user_id => @user, :context => @user, :media_id => "not_in_course")
get 'index', params: {:group_id => @group.id, :exclude => ["sources", "tracks"]}
expect(json_parse(response.body)).to match_array([
{
"media_id"=>"in_group",
"media_type"=>nil,
"created_at"=>mo1.created_at.as_json,
"title"=>"Untitled",
"can_add_captions"=>true,
"embedded_iframe_url"=>"http://test.host/media_objects_iframe/in_group"
}
])
end
it "will sort by title" do
course_with_teacher_logged_in
MediaObject.create!(:user_id => @user, :context => @user, :media_id => "test", :title => "ZZZ")