From fda594ba10fe0e7847e27870090ebccddba71a5a Mon Sep 17 00:00:00 2001 From: Brayden Lopez Date: Wed, 11 Feb 2015 17:02:05 -0700 Subject: [PATCH] Folderized homework submissions for Google Drive. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Brad Humphrey QA-Review: Derek Hansen Product-Review: Brad Horrocks --- app/controllers/assignments_controller.rb | 7 +- app/stylesheets/components/_inst_tree.sass | 76 ++++----- app/stylesheets/pages/profile/edit.scss | 12 ++ .../assignments/_submit_assignment.html.erb | 150 +++++++++--------- gems/google_docs/lib/google_docs.rb | 2 + .../lib/google_docs/drive_connection.rb | 78 +++++---- .../lib/google_docs/drive_entry.rb | 104 ++++++++++++ .../lib/google_docs/drive_folder.rb | 41 +++++ public/javascripts/submit_assignment.js | 49 +++++- spec/selenium/assignments_student_spec.rb | 4 +- 10 files changed, 365 insertions(+), 158 deletions(-) create mode 100644 gems/google_docs/lib/google_docs/drive_entry.rb create mode 100644 gems/google_docs/lib/google_docs/drive_folder.rb diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 63efa0b382a..a469d98f8de 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -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) diff --git a/app/stylesheets/components/_inst_tree.sass b/app/stylesheets/components/_inst_tree.sass index 02d31e2d2fc..dac08153b6e 100644 --- a/app/stylesheets/components/_inst_tree.sass +++ b/app/stylesheets/components/_inst_tree.sass @@ -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 \ No newline at end of file + &.loading + background-image: image-url('ajax-loader-small.gif') \ No newline at end of file diff --git a/app/stylesheets/pages/profile/edit.scss b/app/stylesheets/pages/profile/edit.scss index 2670d3a2e97..5c783e2bda3 100644 --- a/app/stylesheets/pages/profile/edit.scss +++ b/app/stylesheets/pages/profile/edit.scss @@ -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; } } diff --git a/app/views/assignments/_submit_assignment.html.erb b/app/views/assignments/_submit_assignment.html.erb index 08b45bd7961..bffc8d00f23 100644 --- a/app/views/assignments/_submit_assignment.html.erb +++ b/app/views/assignments/_submit_assignment.html.erb @@ -201,88 +201,90 @@ #google_docs_tree li.file .popout { float: right; } + .google_doc_form.hide { + display:none !important; + } <% 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]}$}) %> -
-

- - <%= t(:invalid_google_docs_domain, 'Invalid domain') %> -

+ <% 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]}$}) %> +
+

+ + <%= t(:invalid_google_docs_domain, 'Invalid domain') %> +

-

- <%= 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 - %> -

-
- <% 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" %> - - - - - - - - - <%= render :partial => "group_comment" %> - <% if @assignment.turnitin_enabled? %> - <%= render :partial => "turnitin" %> - <% end %> - - - -
- <%= t 'instructions.google_docs', "Select the file from the list below." %> - <%= render :partial => "assignments/group_submission_reminder" if @assignment.has_group_category? %> -
-
-
- <%= image_tag "ajax-loader-bar.gif" %> -
-
-
-
- <%= text_area :submission, :comment, :class => 'submission_comment_textarea', :placeholder => t('comments_placeholder', 'Comments...'), :title => t('additional_comments', 'Additional comments') %> -
-
- - -
- - <% end %> - <% end %> - <% elsif @google_drive_upgrade %> -
- <%= 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:" %> - +

+ <%= 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 + %> +

- <% else %> -
- <%= 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:" %> -
- "><%= t 'links.authorize_google_docs', "Authorize Google Docs Access" %> + <% 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" %> + + + + + + + + + <%= render :partial => "group_comment" %> + <% if @assignment.turnitin_enabled? %> + <%= render :partial => "turnitin" %> + <% end %> + + + +
+ <%= t 'instructions.google_docs', "Select the file from the list below." %> + <%= render :partial => "assignments/group_submission_reminder" if @assignment.has_group_category? %> +
+
+
+ <%= image_tag "ajax-loader-bar.gif" %> +
+
+
+
+ <%= text_area :submission, :comment, :class => 'submission_comment_textarea', :placeholder => t('comments_placeholder', 'Comments...'), :title => t('additional_comments', 'Additional comments') %> +
+
+ + +
+ <% end %> <% end %> + <% if @google_drive_upgrade %> +
+ <%= 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:" %> + +
+ <% else %> +
+ <%= 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:" %> + +
+ <% end %> + <% end %> <% if @assignment.submission_types && @assignment.submission_types.match(/media_recording/) %> <% if !feature_enabled?(:kaltura) %> diff --git a/gems/google_docs/lib/google_docs.rb b/gems/google_docs/lib/google_docs.rb index 4de8fdff3a1..0e9ef88c53a 100644 --- a/gems/google_docs/lib/google_docs.rb +++ b/gems/google_docs/lib/google_docs.rb @@ -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" diff --git a/gems/google_docs/lib/google_docs/drive_connection.rb b/gems/google_docs/lib/google_docs/drive_connection.rb index 94edb52304f..e1df1b5d8dc 100644 --- a/gems/google_docs/lib/google_docs/drive_connection.rb +++ b/gems/google_docs/lib/google_docs/drive_connection.rb @@ -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 diff --git a/gems/google_docs/lib/google_docs/drive_entry.rb b/gems/google_docs/lib/google_docs/drive_entry.rb new file mode 100644 index 00000000000..d36117e3eb8 --- /dev/null +++ b/gems/google_docs/lib/google_docs/drive_entry.rb @@ -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 . +# +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 diff --git a/gems/google_docs/lib/google_docs/drive_folder.rb b/gems/google_docs/lib/google_docs/drive_folder.rb new file mode 100644 index 00000000000..1854a867d37 --- /dev/null +++ b/gems/google_docs/lib/google_docs/drive_folder.rb @@ -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 \ No newline at end of file diff --git a/public/javascripts/submit_assignment.js b/public/javascripts/submit_assignment.js index 9543e2de3dd..4a1e61d623e 100644 --- a/public/javascripts/submit_assignment.js +++ b/public/javascripts/submit_assignment.js @@ -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); } diff --git a/spec/selenium/assignments_student_spec.rb b/spec/selenium/assignments_student_spec.rb index 985623195af..2e2b0b6d300 100755 --- a/spec/selenium/assignments_student_spec.rb +++ b/spec/selenium/assignments_student_spec.rb @@ -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