Folderized homework submissions for Google Drive.

Fixes PLAT-912
Fixes PLAT-915
Fixes PLAT-916
Fixes PLAT-922

Test Plan:
 - Go to submit an assignment.
 - Test plan for Authorization in Popup Modal:
	- Make sure Google Drive service is inactive
	- Go to Google Drive tab and make sure it asks for authorization
	- Make sure when “Authorize” button is clicked, Google’s OAuth opens in modal
	- After authorized, make sure the panel reloads with the correct content.
 - Test plan for folderifying documents:
	- Make sure Google Drive submissions displays the correct folders and icons
 - Test plan for preview fix:
	- Make sure when you go to preview an assignment for Google Docs, it launches the correct url.
 - Test plan for submissions:
	- Make sure you can submit assignments via Google Drive
 - Test plan for UI/UX:
	- Make sure anything that uses _inst_tree.sass looks correct

Change-Id: Iba790947cefac1420d8d3261865a52f82484a0ef
Reviewed-on: https://gerrit.instructure.com/48981
Tested-by: Jenkins
Reviewed-by: Brad Horrocks <bhorrocks@instructure.com>
Reviewed-by: Brad Humphrey <brad@instructure.com>
QA-Review: Derek Hansen <dhansen@instructure.com>
Product-Review: Brad Horrocks <bhorrocks@instructure.com>
This commit is contained in:
Brayden Lopez 2015-02-11 17:02:05 -07:00 committed by Brad Horrocks
parent ec96119429
commit fda594ba10
10 changed files with 365 additions and 158 deletions

View File

@ -132,12 +132,13 @@ class AssignmentsController < ApplicationController
google_docs = google_service_connection
@google_service = google_docs.service_type
@google_docs_token = google_docs.retrieve_access_token
@google_drive_upgrade = logged_in_user && Canvas::Plugin.find(:google_drive).try(:settings) &&
(!logged_in_user.user_services.where(service: 'google_drive').first || !(google_drive_connection.verify_access_token rescue false))
rescue GoogleDocs::NoTokenError
#do nothing
# Just fail I guess.
end
@google_drive_upgrade = !!(logged_in_user && Canvas::Plugin.find(:google_drive).try(:settings) &&
(!logged_in_user.user_services.where(service: 'google_drive').first || !(google_docs.verify_access_token rescue false)))
add_crumb(@assignment.title, polymorphic_url([@context, @assignment]))
log_asset_access(@assignment, "assignments", @assignment.assignment_group)

View File

@ -21,6 +21,7 @@ ul.instTree
position: relative
border-radius: 3px
background: 13px top no-repeat
background-size: 16px 16px
&.separator
min-height: 2px
@ -28,17 +29,18 @@ ul.instTree
font-size: 2px
&.node
background-image: url(/images/inst_tree/folder.png)
background-image: image-url("mimeClassIcons/folder.svg")
&.collaborations
background-image: url(/images/collaboration_folder.png)
background-image: image-url('collaboration_folder.png')
&.groups
background-image: url(/images/groups_folder.png)
background-image: image-url('groups_folder.png')
// &.open
&.leaf
background-image: url(/images/inst_tree/file_types/page_white.png)
background-image: image-url('mimeClassIcons/file.svg')
&:hover
background-color: #eee
@ -73,10 +75,10 @@ ul.instTree
top: 0
&.plus
background: url(/images/inst_tree/plus.gif) 5px 50% no-repeat
background: image-url('inst_tree/plus.gif') 5px 50% no-repeat
&.minus
background: url(/images/inst_tree/minus.gif) 5px 50% no-repeat
background: image-url('inst_tree/minus.gif') 5px 50% no-repeat
input
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif
@ -87,57 +89,57 @@ ul.instTree
#instTree-drag
padding: 3px
padding-left: 30px
padding: 3px 3px 3px 30px
z-index: 1000
position: absolute
&.node
background: #C3E1FF url(/images/inst_tree/node-drag.gif) left 3px no-repeat
background: #C3E1FF image-url('inst_tree/node-drag.gif') left 3px no-repeat
&.leaf
background: #C3E1FF url(/images/inst_tree/leaf-drag.gif) left 3px no-repeat
background: #C3E1FF image-url('inst_tree/leaf-drag.gif') left 3px no-repeat
ul.instTree
li.separator.dd-hover
background: transparent url(/images/inst_tree/separator-drag.gif) 3px 1px no-repeat
background: transparent image-url('inst_tree/separator-drag.gif') 3px 1px no-repeat
.dd-hover
background-color: yellow
li.leaf
&.dd-hover
background-color: yellow
.pdf
background-image: url('/images/inst_tree/file_types/page_white_acrobat.png') !important
&.pdf
background-image: image-url('mimeClassIcons/pdf.svg')
.image
background-image: url('/images/inst_tree/file_types/page_white_picture.png') !important
&.image, &.jpeg, &.jpg, &.png, &.svg
background-image: image-url('mimeClassIcons/image.svg')
.xls
background-image: url('/images/inst_tree/file_types/page_white_excel.png') !important
&.xls, &.xlsx, &.spreadsheet
background-image: image-url('mimeClassIcons/xls.svg')
.word, .doc
background-image: url('/images/inst_tree/file_types/page_white_word.png') !important
&.word, &.doc, &.docx
background-image: image-url('mimeClassIcons/doc.svg')
.ppt
background-image: url('/images/inst_tree/file_types/page_white_powerpoint.png') !important
&.ppt, &.pptx
background-image: image-url('mimeClassIcons/ppt.svg')
.zip
background-image: url('/images/inst_tree/file_types/page_white_zip.png') !important
&.audio
background-image: image-url('mimeClassIcons/audio.svg')
.video
background-image: url('/images/inst_tree/file_types/page_white_camera.png') !important
&.zip
background-image: image-url('mimeClassIcons/zip.svg')
.html
background-image: url('/images/inst_tree/file_types/page_white_world.png') !important
&.video
background-image: image-url('mimeClassIcons/video.svg')
.spreadsheet
background-image: url('/images/inst_tree/file_types/page_white_excel.png') !important
&.html
background-image: image-url('mimeClassIcons/html.svg')
.google_docs
background-image: url('/images/google_docs_icon.ico') !important
&.google_docs
background-image: image-url('google_docs_icon.ico')
.etherpad
background-image: url('/images/etherpad_icon.ico') !important
&.etherpad
background-image: image-url('etherpad_icon.ico')
.loading
background-image: url('/images/ajax-loader-small.gif') !important
&.loading
background-image: image-url('ajax-loader-small.gif')

View File

@ -44,6 +44,16 @@
&.service-hover .delete_service_link {
display: inline;
img {
width: auto;
height: auto;
}
}
img {
width: 30px;
height: 30px;
}
}
}
@ -55,6 +65,8 @@
img {
vertical-align: middle;
width: 30px;
height: 30px;
}
}

View File

@ -201,88 +201,90 @@
#google_docs_tree li.file .popout {
float: right;
}
.google_doc_form.hide {
display:none !important;
}
</style>
<% if show_google_docs %>
<% if @google_docs_token and not @google_drive_upgrade%>
<% if @domain_root_account.feature_enabled?(:google_docs_domain_restriction) &&
@domain_root_account.settings[:google_docs_domain] &&
!@current_user.gmail.match(%r{@#{@domain_root_account.settings[:google_docs_domain]}$}) %>
<div id="submit_google_doc_form">
<p class="alert alert-error">
<i class="icon-warning"></i>
<%= t(:invalid_google_docs_domain, 'Invalid domain') %>
</p>
<% if @domain_root_account.feature_enabled?(:google_docs_domain_restriction) &&
@domain_root_account.settings[:google_docs_domain] &&
!@current_user.gmail.match(%r{@#{@domain_root_account.settings[:google_docs_domain]}$}) %>
<div id="submit_google_doc_form" class="google_doc_form <%= @google_docs_token ? '' : 'hide' %>">
<p class="alert alert-error">
<i class="icon-warning"></i>
<%= t(:invalid_google_docs_domain, 'Invalid domain') %>
</p>
<p>
<%= t(:gmail_restriction_description, <<-END, domain: @domain_root_account.settings[:google_docs_domain])
Your account has restricted Google Doc submissions to Google accounts
on the %{domain} domain. To submit this assignment with a Google Doc,
you will need to reconfigure the Google Docs integration on your
user settings page.
END
%>
</p>
</div>
<% else %>
<%= form_tag(context_url(@context, :controller => :submissions, :assignment_id => @assignment.id, :action => :create), {:id => "submit_google_doc_form", :class => "submit_assignment_form"}) do %>
<%= hidden_field :submission, :submission_type, :value => "google_doc" %>
<%= hidden_field :google_doc, :document_id, :value => "", :class => "google_doc_id" %>
<table class="formtable" style="width: 100%;">
<tr>
<td style="padding-bottom: 10px;" colspan="2">
<%= t 'instructions.google_docs', "Select the file from the list below." %>
<%= render :partial => "assignments/group_submission_reminder" if @assignment.has_group_category? %>
</td>
</tr><tr>
<td colspan="2">
<div id="google_docs_container" style="height: 200px; overflow: auto;">
<div style="text-align: center; margin: 10px;">
<%= image_tag "ajax-loader-bar.gif" %>
</div>
</div>
</td>
</tr><tr>
<td colspan="2" style="text-align: center;">
<div style="text-align: left;">
<%= text_area :submission, :comment, :class => 'submission_comment_textarea', :placeholder => t('comments_placeholder', 'Comments...'), :title => t('additional_comments', 'Additional comments') %>
</div>
</td>
</tr>
<%= render :partial => "group_comment" %>
<% if @assignment.turnitin_enabled? %>
<%= render :partial => "turnitin" %>
<% end %>
<tr>
<td colspan="2" class='button-container'>
<button type="button" class='cancel_button btn'><%= t '#buttons.cancel', "Cancel" %></button>
<button type="submit" class="btn btn-primary"><%= t 'buttons.submit_assignment', "Submit Assignment" %></button>
</td>
</tr>
</table>
<div id="uploading_google_doc_message" style="display: none;">
<%= t 'messages.uploading', "Retrieving a copy of your Google Doc to submit for this assignment. This may take a little while, depending on the size of the file..." %>
<div style="text-align: center; margin: 10px;">
<%= image_tag "ajax-loader-bar.gif" %>
</div>
</div>
<% end %>
<% end %>
<% elsif @google_drive_upgrade %>
<div id="submit_google_doc_form">
<%= t 'messages.google_drives_auth_required', "Before you can submit assignments directly from Google Drive you need to authorize Canvas to access your Google Drive account:" %>
<div style="font-size: 1.1em; text-align: center; margin: 10px;">
<a class="btn" href="<%= oauth_url(:service => :google_drive, :return_to => (request.url + "#submit_google_doc_form")) %>"><%= t 'links.authorize_google_drive', "Authorize Google Drive Access" %></a>
</div>
<p>
<%= t(:gmail_restriction_description, <<-END, domain: @domain_root_account.settings[:google_docs_domain])
Your account has restricted Google Doc submissions to Google accounts
on the %{domain} domain. To submit this assignment with a Google Doc,
you will need to reconfigure the Google Docs integration on your
user settings page.
END
%>
</p>
</div>
<% else %>
<div id="submit_google_doc_form">
<%= t 'messages.google_docs_auth_required', "Before you can submit assignments directly from Google Docs you need to authorize Canvas to access your Google Docs account:" %>
<div style="font-size: 1.1em; text-align: center; margin: 10px;">
<a class="btn" href="<%= oauth_url(:service => :google_docs, :return_to => (request.url + "#submit_google_doc_form")) %>"><%= t 'links.authorize_google_docs', "Authorize Google Docs Access" %></a>
<% else %>
<%= form_tag(context_url(@context, :controller => :submissions, :assignment_id => @assignment.id, :action => :create), { :id => "submit_google_doc_form", :class => "submit_assignment_form google_doc_form #{@google_docs_token ? '' : 'hide'}"}) do %>
<%= hidden_field :submission, :submission_type, :value => "google_doc" %>
<%= hidden_field :google_doc, :document_id, :value => "", :class => "google_doc_id" %>
<table class="formtable" style="width: 100%;">
<tr>
<td style="padding-bottom: 10px;" colspan="2">
<%= t 'instructions.google_docs', "Select the file from the list below." %>
<%= render :partial => "assignments/group_submission_reminder" if @assignment.has_group_category? %>
</td>
</tr><tr>
<td colspan="2">
<div id="google_docs_container" style="height: 200px; overflow: auto;">
<div style="text-align: center; margin: 10px;">
<%= image_tag "ajax-loader-bar.gif" %>
</div>
</div>
</td>
</tr><tr>
<td colspan="2" style="text-align: center;">
<div style="text-align: left;">
<%= text_area :submission, :comment, :class => 'submission_comment_textarea', :placeholder => t('comments_placeholder', 'Comments...'), :title => t('additional_comments', 'Additional comments') %>
</div>
</td>
</tr>
<%= render :partial => "group_comment" %>
<% if @assignment.turnitin_enabled? %>
<%= render :partial => "turnitin" %>
<% end %>
<tr>
<td colspan="2" class='button-container'>
<button type="button" class='cancel_button btn'><%= t '#buttons.cancel', "Cancel" %></button>
<button type="submit" class="btn btn-primary"><%= t 'buttons.submit_assignment', "Submit Assignment" %></button>
</td>
</tr>
</table>
<div id="uploading_google_doc_message" style="display: none;">
<%= t 'messages.uploading', "Retrieving a copy of your Google Doc to submit for this assignment. This may take a little while, depending on the size of the file..." %>
<div style="text-align: center; margin: 10px;">
<%= image_tag "ajax-loader-bar.gif" %>
</div>
</div>
<% end %>
<% end %>
<% if @google_drive_upgrade %>
<div id="submit_google_doc_form" class="google_doc_form auth <%= @google_docs_token ? 'hide' : '' %>">
<%= t 'messages.google_drives_auth_required', "Before you can submit assignments directly from Google Drive you need to authorize Canvas to access your Google Drive account:" %>
<div style="font-size: 1.1em; text-align: center; margin: 10px;">
<a class="btn" id="auth-google" href="<%= oauth_url(:service => :google_drive, :return_to => (request.url + "#submit_google_doc_form")) %>"><%= t 'links.authorize_google_drive', "Authorize Google Drive Access" %></a>
</div>
</div>
<% else %>
<div id="submit_google_doc_form" class="google_doc_form auth <%= @google_docs_token ? 'hide' : '' %>">
<%= t 'messages.google_docs_auth_required', "Before you can submit assignments directly from Google Docs you need to authorize Canvas to access your Google Docs account:" %>
<div style="font-size: 1.1em; text-align: center; margin: 10px;">
<a class="btn" id="auth-google" href="<%= oauth_url(:service => :google_docs, :return_to => (request.url + "#submit_google_doc_form")) %>"><%= t 'links.authorize_google_docs', "Authorize Google Docs Access" %></a>
</div>
</div>
<% end %>
<% end %>
<% if @assignment.submission_types && @assignment.submission_types.match(/media_recording/) %>
<% if !feature_enabled?(:kaltura) %>

View File

@ -5,6 +5,8 @@ require 'uri'
module GoogleDocs
require "google_docs/connection"
require "google_docs/drive_connection"
require "google_docs/drive_entry"
require "google_docs/drive_folder"
require "google_docs/entry"
require "google_docs/folder"
require "google_docs/no_token_error"

View File

@ -33,7 +33,7 @@ module GoogleDocs
end
def service_type
:google_drive
'google_drive'
end
def download(document_id)
@ -43,7 +43,7 @@ module GoogleDocs
)
file = response.data
file_info = get_file_info(file)
file_info = GoogleDocs::DriveEntry.get_file_data(file)
result = api_client.execute(:uri => file_info[:url])
if result.status == 200
@ -139,7 +139,7 @@ module GoogleDocs
def verify_access_token
api_client.authorization.update_token!
return api_client.execute(:api_method => drive.about.get).status == 200
api_client.execute(:api_method => drive.about.get).status == 200
end
def self.config_check(settings)
@ -158,26 +158,40 @@ module GoogleDocs
end
private
def list(extensions)
documents = api_client.execute!(:api_method => drive.files.list).data.to_hash
{
:name => '/',
:folders => [],
:files => documents['items'].map do |doc|
doc_info = get_file_info(doc)
if extensions.include?(doc_info[:ext])
{
:name => doc['title'],
:document_id => doc['id'],
:extension => doc_info[:ext],
:alternate_url => {
:href => doc_info[:url]
}
}
end
end.compact!
}
def list(extensions)
folderize_list(api_client.execute!(:api_method => drive.files.list, :parameters => {:maxResults => 0}).data.to_hash, extensions)
end
def folderize_list(documents, extensions)
root = GoogleDocs::DriveFolder.new('/')
folders = {nil => root}
documents['items'].each do |doc_entry|
entry = GoogleDocs::DriveEntry.new(doc_entry)
if folders.has_key?(entry.folder)
folder = folders[entry.folder]
else
folder = GoogleDocs::DriveFolder.new(get_folder_name_by_id(documents['items'], entry.folder))
root.add_folder folder
folders[entry.folder] = folder
end
folder.add_file(entry) unless doc_entry['mimeType'] && doc_entry['mimeType'] == 'application/vnd.google-apps.folder'
end
if extensions && extensions.length > 0
root.select { |e| extensions.include?(e.extension) }
else
root
end
end
def get_folder_name_by_id(entries, folder_id)
elements = entries.select do |entry|
entry['id'] == folder_id
end
elements.first ? elements.first['title'] : 'Unknown Folder'
end
def api_client
@ -189,26 +203,6 @@ module GoogleDocs
api_client.discovered_api('drive', 'v2')
end
def get_file_info(file)
file_ext = {
".docx" => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
".xlsx" => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
".pdf" => 'application/pdf',
}
e = file_ext.find {|extension, mime_type| file['exportLinks'] && file['exportLinks'][mime_type] }
if e.present?
{
url: file['exportLinks'][e.last],
ext: e.first
}
else
{
url: file['downloadUrl'],
ext: ".none"
}
end
end
end
end

View File

@ -0,0 +1,104 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module GoogleDocs
class DriveEntry
attr_reader :document_id, :folder, :entry
def initialize(google_drive_entry)
@entry = google_drive_entry
@document_id = @entry['id']
parent = @entry['parents'].length > 0 ? @entry['parents'][0] : nil
@folder = (parent == nil || parent['isRoot'] ? nil : parent['id'])
end
def alternate_url
@entry['alternateLink'] || 'http://docs.google.com'
end
def edit_url
alternate_url rescue "https://docs.google.com/document/d/#{@document_id}/edit?usp=drivesdk"
end
def self.get_file_data(file)
# Order is important.
file_ext = {
#documents
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'doc' => 'application/vnd.oasis.opendocument.text',
#presentations
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
#sheets
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xls' => 'application/x-vnd.oasis.opendocument.spreadsheet',
#PDF
'pdf' => 'application/pdf',
#zip
'zip' => 'application/zip',
}
e = file_ext.find do |extension, mime_type|
(file['exportLinks'] && file['exportLinks'][mime_type]) ||
(file['downloadUrl'] && mime_type.match(file['mimeType']))
end
if e.present?
{
url: (file['exportLinks'] && file['exportLinks'][e.last]) || file['downloadUrl'],
ext: e.first
}
else
{
url: file['downloadUrl'],
ext: 'none'
}
end
end
def extension
GoogleDocs::DriveEntry.get_file_data(@entry)[:ext]
end
def display_name
@entry['title'] || "google_doc.#{extension}"
end
def download_url
GoogleDocs::DriveEntry.get_file_data(@entry)[:url]
end
def to_hash
{
:name => display_name,
:document_id => @document_id,
:extension => extension,
:alternate_url => {:href => alternate_url}
}
end
end
end

View File

@ -0,0 +1,41 @@
module GoogleDocs
class DriveFolder
attr_reader :name, :folders, :files
def initialize(name, folders=[], files=[])
@name = name
@folders, @files = folders, files
end
def add_file(file)
@files << file
end
def add_folder(folder)
@folders << folder
end
def select(&block)
DriveFolder.new(@name,
@folders.map { |f| f.select(&block) }.select { |f| !f.files.empty? },
@files.select(&block))
end
def map(&block)
@folders.map { |f| f.map(&block) }.flatten +
@files.map(&block)
end
def flatten
@folders.flatten + @files
end
def to_hash
{
:name => @name,
:folders => @folders.map { |sf| sf.to_hash },
:files => @files.map { |f| f.to_hash }
}
end
end
end

View File

@ -159,7 +159,7 @@ define([
if(hash && hash.indexOf("#submit") == 0) {
$(".submit_assignment_link").triggerHandler('click', true);
if(hash == "#submit_google_doc") {
$("#submit_assignment_tabs").tabs('select', "#submit_google_doc_form");
$("#submit_assignment_tabs").tabs('select', ".google_doc_form");
}
}
});
@ -280,6 +280,53 @@ define([
listGoogleDocs();
}
$("#auth-google").live('click', function(e){
e.preventDefault();
var href = $(this).attr("href");
reauth(href);
});
// Yay for IE!
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventHandler = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
// Post message for anybody to listen to //
if (window.opener)
window.opener.postMessage({
type: "event",
payload: "done"
}, window.location.origin);
function reauth(auth_url) {
var modal;
if(window.showModalDialog) {
modal = window.showModalDialog(auth_url, "Authorize Google Docs");
} else {
modal = window.open(auth_url, "Authorize Google Docs", 'menubar=no;directories=no;location=no;modal=yes');
}
eventHandler(messageEvent, function(event) {
if(!event || !event.data || event.origin !== window.location.origin) return;
if(event.data.type == "event" && event.data.payload == "done") {
if (modal)
modal.close();
reloadGoogleDrive();
}
}, false);
}
function reloadGoogleDrive() {
$("#submit_google_doc_form.auth").hide();
$("#submit_google_doc_form.submit_assignment_form").removeClass('hide');
listGoogleDocs();
}
function toggleRemoveAttachmentLinks(){
$('#submit_online_upload_form .remove_attachment_link').showIf($('#submit_online_upload_form .submission_attachment:not(#submission_attachment_blank)').length > 1);
}

View File

@ -68,7 +68,9 @@ describe "assignments" do
get "/courses/#{@course.id}/assignments/#{assignment.id}"
expect(ffj('.formtable input[name="submission[group_comment]"]').size).to eq 3
acceptable_tabs = ffj('#submit_online_upload_form,#submit_online_text_entry_form,#submit_online_url_form')
expect(acceptable_tabs.size).to be 3
acceptable_tabs.each { |tabby| expect(ffj('.formtable input[name="submission[group_comment]"]', tabby).size).to be 1 }
end
it "should not show assignments in an unpublished course" do