+ );
+ }
+ });
+
+ return ApiProgressBar;
+});
diff --git a/app/jsx/shared/helpers/createStore.jsx b/app/jsx/shared/helpers/createStore.jsx
index 0958a9fd0df..c6098b99546 100644
--- a/app/jsx/shared/helpers/createStore.jsx
+++ b/app/jsx/shared/helpers/createStore.jsx
@@ -48,6 +48,11 @@ define(['underscore', 'Backbone'], function(_, Backbone) {
return state;
},
+ clearState() {
+ state = {};
+ this.emitChange()
+ },
+
addChangeListener (listener) {
events.on('change', listener);
},
diff --git a/app/jsx/shared/stores/ProgressStore.jsx b/app/jsx/shared/stores/ProgressStore.jsx
new file mode 100644
index 00000000000..70f03cfac5a
--- /dev/null
+++ b/app/jsx/shared/stores/ProgressStore.jsx
@@ -0,0 +1,22 @@
+/** @jsx */
+
+define([
+ 'react',
+ 'underscore',
+ 'jsx/shared/helpers/createStore',
+ 'jquery'
+], (React, _, createStore, $) => {
+ var ProgressStore = createStore({}),
+ _progresses = {};
+
+ ProgressStore.get = function(progress_id) {
+ var url = "/api/v1/progress/" + progress_id;
+
+ $.getJSON(url, function(data) {
+ _progresses[data.id] = data;
+ ProgressStore.setState(_progresses);
+ });
+ };
+
+ return ProgressStore;
+})
diff --git a/app/models/content_export.rb b/app/models/content_export.rb
index 14ca65a86e6..3121730bf49 100644
--- a/app/models/content_export.rb
+++ b/app/models/content_export.rb
@@ -19,14 +19,14 @@
class ContentExport < ActiveRecord::Base
include Workflow
belongs_to :context, :polymorphic => true
-
belongs_to :user
belongs_to :attachment
belongs_to :content_migration
has_many :attachments, :as => :context, :dependent => :destroy
+ has_one :epub_export
has_a_broadcast_policy
serialize :settings
- attr_accessible :context
+ attr_accessible :context, :export_type, :user, :selected_content, :progress
validates_presence_of :context_id, :workflow_state
validates_inclusion_of :context_type, :in => ['Course', 'Group', 'User']
@@ -61,7 +61,7 @@ class ContentExport < ActiveRecord::Base
p.whenever {|record|
record.changed_state(:exported) && record.send_notification?
}
-
+
p.dispatch :content_export_failed
p.to { [user] }
p.whenever {|record|
@@ -127,6 +127,7 @@ class ContentExport < ActiveRecord::Base
self.job_progress.try :fail!
ensure
self.save
+ epub_export.try(:mark_exported) || true
end
end
@@ -200,11 +201,11 @@ class ContentExport < ActiveRecord::Base
def zip_export?
self.export_type == ZIP
end
-
+
def error_message
self.settings[:errors] ? self.settings[:errors].last : nil
end
-
+
def error_messages
self.settings[:errors] ||= []
end
@@ -226,8 +227,8 @@ class ContentExport < ActiveRecord::Base
end
# Method Summary
- # Takes in an ActiveRecord object. Determines if the item being
- # checked should be exported or not.
+ # Takes in an ActiveRecord object. Determines if the item being
+ # checked should be exported or not.
#
# Returns: bool
def export_object?(obj, asset_type=nil)
@@ -249,7 +250,7 @@ class ContentExport < ActiveRecord::Base
# Takes a symbol containing the items that were selected to export.
# is_set? will return true if the item is selected. Also handles
# a case where 'everything' is set and returns true
- #
+ #
# Returns: bool
def export_symbol?(symbol)
selected_content.empty? || is_set?(selected_content[symbol]) || is_set?(selected_content[:everything])
@@ -264,7 +265,7 @@ class ContentExport < ActiveRecord::Base
selected_content[asset_type] ||= {}
selected_content[asset_type][select_content_key(obj)] = true
end
-
+
def add_error(user_message, exception_or_info=nil)
self.settings[:errors] ||= []
er = nil
@@ -283,11 +284,11 @@ class ContentExport < ActiveRecord::Base
def root_account
self.context.try_rescue(:root_account)
end
-
+
def running?
['created', 'exporting'].member? self.workflow_state
end
-
+
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
@@ -298,27 +299,35 @@ class ContentExport < ActiveRecord::Base
def settings
read_attribute(:settings) || write_attribute(:settings,{}.with_indifferent_access)
end
-
+
def fast_update_progress(val)
content_migration.update_conversion_progress(val) if content_migration
self.progress = val
ContentExport.where(:id => self).update_all(:progress=>val)
self.job_progress.try(:update_completion!, val)
end
-
- scope :active, -> { where("workflow_state<>'deleted'") }
- scope :not_for_copy, -> { where("export_type<>?", COURSE_COPY) }
- scope :common_cartridge, -> { where(:export_type => COMMON_CARTRIDGE) }
- scope :qti, -> { where(:export_type => QTI) }
- scope :course_copy, -> { where(:export_type => COURSE_COPY) }
- scope :running, -> { where(:workflow_state => ['created', 'exporting']) }
- scope :admin, ->(user) { where("export_type NOT IN (?) OR user_id=?", [ZIP, USER_DATA], user) }
- scope :non_admin, ->(user) { where("export_type IN (?) AND user_id=?", [ZIP, USER_DATA], user) }
+
+ scope :active, -> { where("content_exports.workflow_state<>'deleted'") }
+ scope :not_for_copy, -> { where("content_exports.export_type<>?", COURSE_COPY) }
+ scope :common_cartridge, -> { where(export_type: COMMON_CARTRIDGE) }
+ scope :qti, -> { where(export_type: QTI) }
+ scope :course_copy, -> { where(export_type: COURSE_COPY) }
+ scope :running, -> { where(workflow_state: ['created', 'exporting']) }
+ scope :admin, ->(user) {
+ where("content_exports.export_type NOT IN (?) OR content_exports.user_id=?", [
+ ZIP, USER_DATA
+ ], user)
+ }
+ scope :non_admin, ->(user) {
+ where("content_exports.export_type IN (?) AND content_exports.user_id=?", [
+ ZIP, USER_DATA
+ ], user)
+ }
+ scope :without_epub, -> {eager_load(:epub_export).where(epub_exports: {id: nil})}
private
-
def is_set?(option)
Canvas::Plugin::value_to_boolean option
end
-
+
end
diff --git a/app/models/course.rb b/app/models/course.rb
index 15059726604..b28f2b8ab3b 100644
--- a/app/models/course.rb
+++ b/app/models/course.rb
@@ -75,7 +75,8 @@ class Course < ActiveRecord::Base
:lock_all_announcements,
:public_syllabus,
:course_format,
- :time_zone
+ :time_zone,
+ :organize_epub_by_content_type
EXPORTABLE_ATTRIBUTES = [
:id, :name, :account_id, :group_weighting_scheme, :workflow_state, :uuid, :start_at, :conclude_at, :grading_standard_id, :is_public, :allow_student_wiki_edits,
@@ -202,6 +203,7 @@ class Course < ActiveRecord::Base
has_many :role_overrides, :as => :context
has_many :content_migrations, :as => :context
has_many :content_exports, :as => :context
+ has_many :epub_exports, order: :created_at
has_many :course_imports
has_many :alerts, as: :context, preload: :criteria
has_many :appointment_group_contexts, :as => :context
@@ -1990,7 +1992,8 @@ class Course < ActiveRecord::Base
:storage_quota, :tab_configuration, :allow_wiki_comments,
:turnitin_comments, :self_enrollment, :license, :indexed, :locale,
:hide_final_grade, :hide_distribution_graphs,
- :allow_student_discussion_topics, :allow_student_discussion_editing, :lock_all_announcements ]
+ :allow_student_discussion_topics, :allow_student_discussion_editing, :lock_all_announcements,
+ :organize_epub_by_content_type ]
end
def set_course_dates_if_blank(shift_options)
@@ -2532,6 +2535,7 @@ class Course < ActiveRecord::Base
add_setting :large_roster, :boolean => true, :default => lambda { |c| c.root_account.large_course_rosters? }
add_setting :public_syllabus, :boolean => true, :default => false
add_setting :course_format
+ add_setting :organize_epub_by_content_type, :boolean => true, :default => false
add_setting :is_public_to_auth_users, :boolean => true, :default => false
add_setting :restrict_student_future_view, :boolean => true, :inherited => true
diff --git a/app/models/epub_export.rb b/app/models/epub_export.rb
new file mode 100644
index 00000000000..08481c43bfd
--- /dev/null
+++ b/app/models/epub_export.rb
@@ -0,0 +1,111 @@
+class EpubExport < ActiveRecord::Base
+ include Workflow
+
+ belongs_to :content_export
+ belongs_to :course
+ belongs_to :user
+ has_one :attachment, as: :context, dependent: :destroy
+ has_one :job_progress, as: :context, class_name: 'Progress'
+ validates :course_id, :workflow_state, presence: true
+
+ PERCENTAGE_COMPLETE = {
+ created: 0,
+ exporting: 25,
+ exported: 50,
+ generating: 75,
+ generated: 100
+ }.freeze
+
+ workflow do # percentage completion
+ state :created # 0%
+ state :exporting # 25%
+ state :exported # 50%
+ state :generating # 75%
+ state :generated # 100%
+ state :failed
+ state :deleted
+ end
+
+ after_create do
+ create_job_progress(completion: 0, tag: 'epub_export')
+ end
+
+ delegate :download_url, to: :attachment, allow_nil: true
+ delegate :completion, :running?, to: :job_progress, allow_nil: true
+
+ scope :running, -> { where(workflow_state: ['created', 'exporting', 'exported', 'generating']) }
+ scope :visible_to, ->(user) { where(user_id: user) }
+
+ set_policy do
+ given do |user|
+ course.grants_right?(user, :read_as_admin) ||
+ course.grants_right?(user, :participate_as_student)
+ end
+ can :create
+
+ given do |user|
+ self.user == user || course.grants_right?(user, :read_as_admin)
+ end
+ can :read
+
+ given do |user|
+ grants_right?(user, :read) && generated?
+ end
+ can :download
+
+ given do |user|
+ [ 'generated', 'failed' ].include?(workflow_state) &&
+ self.grants_right?(user, :create)
+ end
+ can :regenerate
+ end
+
+ def export
+ create_content_export!({
+ user: user,
+ export_type: ContentExport::COMMON_CARTRIDGE,
+ selected_content: { :everything => true },
+ progress: 0,
+ context: course
+ })
+ job_progress.completion = PERCENTAGE_COMPLETE[:exporting]
+ job_progress.start
+ update_attribute(:workflow_state, 'exporting')
+ content_export.export
+ true
+ end
+ handle_asynchronously :export, priority: Delayed::LOW_PRIORITY, max_attempts: 1
+
+ def mark_exported
+ if content_export.failed?
+ fail
+ else
+ update_attribute(:workflow_state, 'exported')
+ job_progress.update_attribute(:completion, PERCENTAGE_COMPLETE[:exported])
+ generate
+ end
+ end
+ handle_asynchronously :mark_exported, priority: Delayed::LOW_PRIORITY, max_attempts: 1
+
+ def generate
+ job_progress.update_attribute(:completion, PERCENTAGE_COMPLETE[:generating])
+ update_attribute(:workflow_state, 'generating')
+ generate_epub
+ end
+ handle_asynchronously :generate, priority: Delayed::LOW_PRIORITY, max_attempts: 1
+
+ def generate_epub
+ success
+ end
+ handle_asynchronously :generate_epub, priority: Delayed::LOW_PRIORITY, max_attempts: 1
+
+ def success
+ job_progress.complete! if job_progress.running?
+ update_attribute(:workflow_state, 'generated')
+ end
+
+ def fail
+ job_progress.try :fail!
+ update_attribute(:workflow_state, 'failed')
+ end
+end
diff --git a/app/models/epub_exports/create_service.rb b/app/models/epub_exports/create_service.rb
new file mode 100644
index 00000000000..d889c682ce5
--- /dev/null
+++ b/app/models/epub_exports/create_service.rb
@@ -0,0 +1,50 @@
+#
+# 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 EpubExports
+ class CreateService
+ def initialize(course, user)
+ @course = course
+ @user = user
+ end
+ attr_reader :course, :user
+
+ def epub_export
+ unless @_epub_export
+ @_epub_export = course.epub_exports.visible_to(user).running.first
+ @_epub_export ||= course.epub_exports.build({
+ user: user
+ })
+ end
+ @_epub_export
+ end
+
+ def already_running?
+ !epub_export.new_record?
+ end
+
+ def save
+ if !already_running? && epub_export.save
+ # Queuing jobs always returns nil, yay
+ epub_export.export
+ true
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/app/models/progress.rb b/app/models/progress.rb
index aaaaf534d39..a3bf4684bf3 100644
--- a/app/models/progress.rb
+++ b/app/models/progress.rb
@@ -20,9 +20,13 @@ class Progress < ActiveRecord::Base
include PolymorphicTypeOverride
override_polymorphic_types context_type: {'QuizStatistics' => 'Quizzes::QuizStatistics'}
+ validates :context_type, inclusion: {
+ in: [
+ 'ContentMigration', 'Course', 'User', 'Quizzes::QuizStatistics', 'Account',
+ 'GroupCategory', 'ContentExport', 'Assignment', 'Attachment', 'EpubExport'
+ ]
+ }, allow_nil: true
belongs_to :context, :polymorphic => true
- validates_inclusion_of :context_type, :allow_nil => true, :in => ['ContentMigration', 'Course', 'User',
- 'Quizzes::QuizStatistics', 'Account', 'GroupCategory', 'ContentExport', 'Assignment', 'Attachment']
belongs_to :user
attr_accessible :context, :tag, :completion, :message
diff --git a/app/stylesheets/bundles/epub_exports.scss b/app/stylesheets/bundles/epub_exports.scss
new file mode 100644
index 00000000000..a5b0d1c6b0f
--- /dev/null
+++ b/app/stylesheets/bundles/epub_exports.scss
@@ -0,0 +1,2 @@
+@import "base/environment";
+@import "components/ProgressBar";
diff --git a/app/views/courses/settings.html.erb b/app/views/courses/settings.html.erb
index c0897b24e6f..06ac3bbf0ec 100644
--- a/app/views/courses/settings.html.erb
+++ b/app/views/courses/settings.html.erb
@@ -273,6 +273,14 @@ TEXT
<%= f.select :course_format, format_options %>
+
+
+
+
+ <%= f.check_box :organize_epub_by_content_type %>
+ <%= f.label :organize_epub_by_content_type, t("Organize epub by content type (default is by module).") %>
+
+ <%= t("Downloading course content allows access to content while offline. \
+ Content may include files, pages, assignments, discussion topics or \
+ quizzes. Click \"Generate ePub\" for each course and open with any \
+ eReader software to view.") %>
+
+
+
+
+
+
+ <%= t("Current Courses") %>
+
+
+
+
+
+
+
diff --git a/app/views/profile/profile.html.erb b/app/views/profile/profile.html.erb
index e2e28dab818..d9c9630e335 100644
--- a/app/views/profile/profile.html.erb
+++ b/app/views/profile/profile.html.erb
@@ -16,6 +16,11 @@
<%= image_tag "unlock.png" %> <%= t('links.disable_mfa', "Disable Multi-Factor Authentication") %>
<% end %>
<%= t("Download Submissions") %>
+ <% if @current_user.feature_enabled?(:epub_export)%>
+ <%= link_to epub_exports_path, class: 'btn button-sidebar-wide' do %>
+ <%= t("Download Course Content") %>
+ <% end %>
+ <% end %>
<% if show_request_delete_account %>
<%= t('Delete My Account') %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 036d2c25431..6acd64031a0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,6 +8,8 @@ Dir["{gems,vendor}/plugins/*/config/pre_routes.rb"].each { |pre_routes|
CanvasRails::Application.routes.draw do
resources :submission_comments, only: :destroy
+ resources :epub_exports, only: [:index]
+
get 'inbox' => 'context#inbox'
get 'oauth/redirect_proxy' => 'oauth_proxy#redirect_proxy'
@@ -1682,6 +1684,18 @@ CanvasRails::Application.routes.draw do
get "courses/:course_id/content_list", action: :content_list, as: "course_content_list"
end
+ scope(controller: :epub_exports) do
+ get 'courses/:course_id/epub_exports/:id', {
+ action: :show
+ }
+ get 'epub_exports', {
+ action: :index
+ }
+ post 'courses/:course_id/epub_exports', {
+ action: :create
+ }
+ end
+
scope(controller: :grading_standards_api) do
get 'courses/:course_id/grading_standards', action: :context_index
get 'accounts/:account_id/grading_standards', action: :context_index
diff --git a/db/migrate/20150715215932_create_epub_exports.rb b/db/migrate/20150715215932_create_epub_exports.rb
new file mode 100644
index 00000000000..d82424eb620
--- /dev/null
+++ b/db/migrate/20150715215932_create_epub_exports.rb
@@ -0,0 +1,23 @@
+class CreateEpubExports < ActiveRecord::Migration
+ tag :predeploy
+ def self.up
+ create_table :epub_exports do |t|
+ t.integer :content_export_id, :course_id, :user_id, limit: 8
+ t.string :workflow_state, default: "created"
+ t.timestamps
+ end
+
+ add_foreign_key_if_not_exists :epub_exports, :users, delay_validation: true
+ add_foreign_key_if_not_exists :epub_exports, :courses, delay_validation: true
+ add_foreign_key_if_not_exists :epub_exports, :content_exports, delay_validation: true
+
+ add_index :epub_exports, :user_id
+ add_index :epub_exports, :course_id
+ add_index :epub_exports, :content_export_id
+
+ end
+
+ def self.down
+ drop_table :epub_exports
+ end
+end
diff --git a/lib/api/v1/epub_export.rb b/lib/api/v1/epub_export.rb
new file mode 100644
index 00000000000..8ed1dd1735b
--- /dev/null
+++ b/lib/api/v1/epub_export.rb
@@ -0,0 +1,46 @@
+#
+# Copyright (C) 2015 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 Api::V1::EpubExport
+ include Api::V1::Attachment
+
+ def course_epub_export_json(course)
+ api_json(course, @current_user, session, {
+ only: [ :name, :id ]
+ }) do |attrs|
+ if course.epub_exports.any?
+ attrs.epub_export = epub_export_json(course.epub_exports.last)
+ end
+ end
+ end
+
+ def epub_export_json(epub_export)
+ api_json(epub_export, @current_user, session, {}, [
+ :download, :regenerate
+ ]) do |attrs|
+ attrs.progress_id = epub_export.job_progress.id
+ attrs.progress_url = polymorphic_url([:api_v1, epub_export.job_progress])
+
+ if epub_export.attachment
+ attrs.attachment = attachment_json(epub_export.attachment, @current_user, {}, {
+ can_view_hidden_files: true
+ })
+ end
+ end
+ end
+end
diff --git a/lib/feature.rb b/lib/feature.rb
index 3bf76bd4638..37b4854936b 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -129,6 +129,15 @@ END
root_opt_in: true,
beta: true
},
+ 'epub_export' =>
+ {
+ display_name: -> { I18n.t('ePub Export') },
+ description: -> { I18n.t(<
{
display_name: -> { I18n.t('features.html5_first_videos', 'Prefer HTML5 for video playback') },
diff --git a/spec/coffeescripts/jsx/epub_exports/AppSpec.coffee b/spec/coffeescripts/jsx/epub_exports/AppSpec.coffee
new file mode 100644
index 00000000000..c239040aae4
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/AppSpec.coffee
@@ -0,0 +1,34 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/epub_exports/App',
+ 'jsx/epub_exports/CourseStore'
+], (_, React, App, CourseEpubExportStore) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'AppSpec',
+ setup: ->
+ @props = {
+ 1: {
+ name: 'Maths 101',
+ id: 1
+ },
+ 2: {
+ name: 'Physics 101',
+ id: 2
+ }
+ }
+ sinon.stub(CourseEpubExportStore, 'getAll', -> true)
+
+ teardown: ->
+ CourseEpubExportStore.getAll.restore()
+
+ test 'handeCourseStoreChange', ->
+ component = TestUtils.renderIntoDocument(App())
+ ok _.isEmpty(component.state), 'precondition'
+
+ CourseEpubExportStore.setState(@props)
+ deepEqual component.state, CourseEpubExportStore.getState(),
+ 'CourseEpubExportStore.setState should trigger component setState'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
diff --git a/spec/coffeescripts/jsx/epub_exports/CourseEpubExportStoreSpec.coffee b/spec/coffeescripts/jsx/epub_exports/CourseEpubExportStoreSpec.coffee
new file mode 100644
index 00000000000..e631e3b5c59
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/CourseEpubExportStoreSpec.coffee
@@ -0,0 +1,76 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/epub_exports/CourseStore'
+], (_, React, CourseStore, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'CourseEpubExportStoreSpec',
+ setup: ->
+ @courses = {
+ courses: [{
+ name: 'Maths 101',
+ id: 1,
+ epub_export: {
+ id: 1
+ }
+ }, {
+ name: 'Physics 101',
+ id: 2
+ }]
+ }
+ @server = sinon.fakeServer.create()
+
+ teardown: ->
+ CourseStore.clearState()
+ @server.restore()
+
+ test 'getAll', ->
+ @server.respondWith('GET', '/api/v1/epub_exports', [
+ 200, {'Content-Type': 'application/json'},
+ JSON.stringify(@courses)
+ ])
+ ok _.isEmpty(CourseStore.getState()), 'precondition'
+ CourseStore.getAll()
+ @server.respond()
+
+ state = CourseStore.getState()
+ _.each(@courses.courses, (course) ->
+ deepEqual state[course.id], course
+ )
+
+ test 'get', ->
+ url = "/api/v1/courses/1/epub_exports/1"
+ course = @courses.courses[0]
+ @server.respondWith('GET', url, [
+ 200, {'Content-Type': 'application/json'},
+ JSON.stringify(course)
+ ])
+ ok _.isEmpty(CourseStore.getState()), 'precondition'
+ CourseStore.get(1, 1)
+ @server.respond()
+
+ state = CourseStore.getState()
+ deepEqual state[course.id], course
+
+ test 'create', ->
+ course_id = 3
+ epub_export = {
+ name: 'Creative Writing',
+ id: course_id,
+ epub_export: {
+ permissions: {},
+ workflow_state: 'created'
+ }
+ }
+ @server.respondWith('POST', '/api/v1/courses/' + course_id + '/epub_exports', [
+ 200, {'Content-Type': 'application/josn'},
+ JSON.stringify(epub_export)
+ ])
+
+ ok _.isUndefined(CourseStore.getState()[course_id]), 'precondition'
+ CourseStore.create(course_id)
+ @server.respond()
+
+ state = CourseStore.getState()
+ deepEqual state[course_id], epub_export, 'should add new object to state'
diff --git a/spec/coffeescripts/jsx/epub_exports/CourseListItemSpec.coffee b/spec/coffeescripts/jsx/epub_exports/CourseListItemSpec.coffee
new file mode 100644
index 00000000000..adc01cb305b
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/CourseListItemSpec.coffee
@@ -0,0 +1,38 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/epub_exports/CourseListItem'
+], (_, React, CourseListItem, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'CourseListItemSpec',
+ setup: ->
+ @props = {
+ course: {
+ name: 'Maths 101',
+ id: 1
+ }
+ }
+
+ test 'getDisplayState', ->
+ component = TestUtils.renderIntoDocument(CourseListItem(@props))
+ ok _.isNull(component.getDisplayState()),
+ 'display state should be null without epub_export'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ @props.course = {
+ epub_export: {
+ permissions: {},
+ workflow_state: 'generating'
+ }
+ }
+ component = TestUtils.renderIntoDocument(CourseListItem(@props))
+ ok !_.isNull(component.getDisplayState()),
+ 'display state should not be null with epub_export'
+ ok component.getDisplayState().match('Generating'), 'should include workflow_state'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'render', ->
+ component = TestUtils.renderIntoDocument(CourseListItem(@props))
+ ok !_.isNull(component.getDOMNode()), 'should render with course'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
diff --git a/spec/coffeescripts/jsx/epub_exports/CourseListSpec.coffee b/spec/coffeescripts/jsx/epub_exports/CourseListSpec.coffee
new file mode 100644
index 00000000000..43e9efb9707
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/CourseListSpec.coffee
@@ -0,0 +1,32 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/epub_exports/CourseList'
+], (_, React, CourseList, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'CourseListSpec',
+ setup: ->
+ @props = {
+ 1: {
+ name: 'Maths 101',
+ id: 1
+ },
+ 2: {
+ name: 'Physics 101',
+ id: 2
+ }
+ }
+
+ test 'render', ->
+ component = TestUtils.renderIntoDocument(CourseList(courses: {}))
+ node = component.getDOMNode()
+ equal node.querySelectorAll('li').length, 0, 'should not render list items'
+ React.unmountComponentAtNode(node.parentNode)
+
+ component = TestUtils.renderIntoDocument(CourseList(courses: @props))
+ node = component.getDOMNode()
+ equal node.querySelectorAll('li').length, Object.keys(@props).length,
+ 'should have an li element per course in @props'
+
+ React.unmountComponentAtNode(node.parentNode)
diff --git a/spec/coffeescripts/jsx/epub_exports/DownloadLinkSpec.coffee b/spec/coffeescripts/jsx/epub_exports/DownloadLinkSpec.coffee
new file mode 100644
index 00000000000..199b20f0da6
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/DownloadLinkSpec.coffee
@@ -0,0 +1,60 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/epub_exports/DownloadLink',
+ 'i18n!epub_exports',
+], (_, React, DownloadLink, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'DownloadLink',
+ setup: ->
+ @props = {
+ course: {
+ name: 'Maths 101',
+ id: 1
+ }
+ }
+
+ test 'state showDownloadLink', ->
+ component = TestUtils.renderIntoDocument(DownloadLink(@props))
+ ok !component.showDownloadLink(), 'should be false without epub_export object'
+
+ @props.course.epub_export = {
+ permissions: {
+ download: false
+ }
+ }
+ component = TestUtils.renderIntoDocument(DownloadLink(@props))
+ ok !component.showDownloadLink(), 'should be false without permissions to download'
+
+ @props.course.epub_export = {
+ attachment: {
+ url: 'http://download.url'
+ },
+ permissions: {
+ download: true
+ }
+ }
+ component = TestUtils.renderIntoDocument(DownloadLink(@props))
+ ok component.showDownloadLink(), 'should be true with permissions to download'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'render', ->
+ component = TestUtils.renderIntoDocument(DownloadLink(@props))
+ node = component.getDOMNode()
+ ok _.isNull(node)
+
+ @props.course.epub_export = {
+ attachment: {
+ url: 'http://download.url'
+ },
+ permissions: {
+ download: true
+ }
+ }
+ component = TestUtils.renderIntoDocument(DownloadLink(@props))
+ node = component.getDOMNode()
+ equal node.tagName, 'A', 'tag should be link'
+ ok node.textContent.match(I18n.t("Download")),
+ 'should show download text'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
diff --git a/spec/coffeescripts/jsx/epub_exports/GenerateLinkSpec.coffee b/spec/coffeescripts/jsx/epub_exports/GenerateLinkSpec.coffee
new file mode 100644
index 00000000000..2b45d2d48ce
--- /dev/null
+++ b/spec/coffeescripts/jsx/epub_exports/GenerateLinkSpec.coffee
@@ -0,0 +1,87 @@
+define [
+ 'jquery',
+ 'react',
+ 'jsx/epub_exports/GenerateLink',
+ 'jsx/epub_exports/CourseStore',
+ 'i18n!epub_exports',
+], ($, React, GenerateLink, CourseEpubExportStore, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'GenerateLink',
+ setup: ->
+ @props = {
+ course: {
+ name: 'Maths 101',
+ id: 1
+ }
+ }
+
+ test 'showGenerateLink', ->
+ component = TestUtils.renderIntoDocument(GenerateLink(@props))
+ ok component.showGenerateLink(), 'should be true without epub_export object'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ @props.course.epub_export = {
+ permissions: {
+ regenerate: false
+ }
+ }
+ component = TestUtils.renderIntoDocument(GenerateLink(@props))
+ ok !component.showGenerateLink(), 'should be false without permissions to rengenerate'
+
+ @props.course.epub_export = {
+ permissions: {
+ regenerate: true
+ }
+ }
+ component = TestUtils.renderIntoDocument(GenerateLink(@props))
+ ok component.showGenerateLink(), 'should be true with permissions to rengenerate'
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'state triggered', ->
+ clock = sinon.useFakeTimers()
+ sinon.stub(CourseEpubExportStore, 'create')
+ component = TestUtils.renderIntoDocument(GenerateLink(@props))
+ node = component.getDOMNode()
+
+ TestUtils.Simulate.click(node)
+ ok component.state.triggered, 'should set state to triggered'
+
+ clock.tick(1005)
+ ok !component.state.triggered, 'should toggle back to not triggered after 1000'
+
+ clock.restore()
+ CourseEpubExportStore.create.restore()
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'render', ->
+ clock = sinon.useFakeTimers()
+ sinon.stub(CourseEpubExportStore, 'create')
+
+ component = TestUtils.renderIntoDocument(GenerateLink(@props))
+ node = component.getDOMNode()
+ equal node.tagName, 'BUTTON', 'tag should be a button'
+ ok node.querySelector('span').textContent.match(I18n.t("Generate ePub")),
+ 'should show generate text'
+
+ TestUtils.Simulate.click(node)
+ node = component.getDOMNode()
+ equal node.tagName, 'SPAN', 'tag should be span'
+ ok node.textContent.match(I18n.t("Generating...")),
+ 'should show generating text'
+
+ @props.course.epub_export = {
+ permissions: {
+ regenerate: true
+ }
+ }
+ component.setProps(@props)
+ clock.tick(2000)
+ node = component.getDOMNode()
+ equal node.tagName, 'BUTTON', 'tag should be a button'
+ ok node.querySelector('span').textContent.match(I18n.t("Regenerate ePub")),
+ 'should show regenerate text'
+
+ clock.restore()
+ CourseEpubExportStore.create.restore()
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
diff --git a/spec/coffeescripts/jsx/shared/ApiProgressBarSpec.coffee b/spec/coffeescripts/jsx/shared/ApiProgressBarSpec.coffee
new file mode 100644
index 00000000000..e8fd350fe45
--- /dev/null
+++ b/spec/coffeescripts/jsx/shared/ApiProgressBarSpec.coffee
@@ -0,0 +1,145 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/shared/ApiProgressBar'
+ 'jsx/shared/stores/ProgressStore'
+], (_, React, ApiProgressBar, ProgressStore) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'ApiProgressBarSpec',
+ setup: ->
+ @progress_id = '1'
+ @progress = {
+ id: @progress_id,
+ context_id: 1,
+ context_type: 'EpubExport',
+ user_id: 1,
+ tag: 'epub_export',
+ completion: 0,
+ workflow_state: 'queued'
+ }
+ @store_state = {}
+ @store_state[@progress_id] = @progress
+ @storeSpy = sinon.stub(ProgressStore, 'get', (=>
+ ProgressStore.setState(@store_state)
+ ))
+ @clock = sinon.useFakeTimers()
+
+ teardown: ->
+ ProgressStore.get.restore()
+ @clock.restore()
+
+ test 'shouldComponentUpdate', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar())
+
+ ok component.shouldComponentUpdate({
+ progress_id: @progress_id
+ }, {}), 'should update when progress_id prop changes'
+
+ ok component.shouldComponentUpdate({}, {
+ workflow_state: 'running'
+ }), 'should update when state changes'
+
+ component.setProps(progress_id: @progress_id)
+ component.setState(workflow_state: 'running')
+
+ ok !component.shouldComponentUpdate({
+ progress_id: @progress_id
+ }, {
+ workflow_state: component.state.workflow_state
+ }), 'should not update if state & props are the same'
+
+ test 'componentDidUpdate', ->
+ onCompleteSpy = sinon.spy()
+ component = TestUtils.renderIntoDocument(ApiProgressBar({
+ onComplete: onCompleteSpy,
+ progress_id: @progress_id
+ }))
+ @clock.tick(component.props.delay + 5)
+ ok !_.isNull(component.intervalID), 'should have interval id'
+
+ @progress.workflow_state = 'running'
+ @clock.tick(component.props.delay + 5)
+ ok !_.isNull(component.intervalID), 'should have an inverval id after updating to running'
+
+ @progress.workflow_state = 'completed'
+ @clock.tick(component.props.delay + 5)
+ ok _.isNull(component.intervalID), 'should not have an inverval id after updating to completed'
+ ok onCompleteSpy.called, 'should call callback on update if complete'
+
+ test 'handleStoreChange', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar({
+ progress_id: @progress_id
+ }))
+ @clock.tick(component.props.delay + 5)
+
+ _.each [ 'completion', 'workflow_state' ], (stateName) =>
+ equal component.state[stateName], @progress[stateName],
+ "component #{stateName} should equal progress #{stateName}"
+
+ @progress.workflow_state = 'running'
+ @progress.completion = 50
+ ProgressStore.setState(@store_state)
+
+ _.each [ 'completion', 'workflow_state' ], (stateName) =>
+ equal component.state[stateName], @progress[stateName],
+ "component #{stateName} should equal progress #{stateName}"
+
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'isComplete', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar({
+ progress_id: @progress_id
+ }))
+ @clock.tick(component.props.delay + 5)
+
+ ok !component.isComplete(), 'is not complete if state is queued'
+
+ @progress.workflow_state = 'running'
+ @clock.tick(component.props.delay + 5)
+ ok !component.isComplete(), 'is not complete if state is running'
+
+ @progress.workflow_state = 'completed'
+ @clock.tick(component.props.delay + 5)
+ ok component.isComplete(), 'is complete if state is completed'
+
+ test 'isInProgress', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar({
+ progress_id: @progress_id
+ }))
+ @clock.tick(component.props.delay + 5)
+
+ ok component.isInProgress(), 'is in progress if state is queued'
+
+ @progress.workflow_state = 'running'
+ @clock.tick(component.props.delay + 5)
+ ok component.isInProgress(), 'is in progress if state is running'
+
+ @progress.workflow_state = 'completed'
+ @clock.tick(component.props.delay + 5)
+ ok !component.isInProgress(), 'is not in progress if state is completed'
+
+ test 'poll', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar())
+ component.poll()
+ ok !@storeSpy.called,
+ 'should not fetch from progress store without progress id'
+
+ component.setProps(progress_id: @progress_id)
+ component.poll()
+ ok @storeSpy.called, 'should fetch when progress id is present'
+
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
+
+ test 'render', ->
+ component = TestUtils.renderIntoDocument(ApiProgressBar({
+ progress_id: @progress_id
+ }))
+ ok _.isNull(component.getDOMNode()),
+ 'should not render to DOM if is not in progress'
+
+ @clock.tick(component.props.delay + 5)
+ ok !_.isNull(component.getDOMNode()),
+ 'should render to DOM if is not in progress'
+
+ React.unmountComponentAtNode(component.getDOMNode().parentNode)
diff --git a/spec/coffeescripts/jsx/shared/stores/ProgressStoreSpec.coffee b/spec/coffeescripts/jsx/shared/stores/ProgressStoreSpec.coffee
new file mode 100644
index 00000000000..59cbe1ac1b8
--- /dev/null
+++ b/spec/coffeescripts/jsx/shared/stores/ProgressStoreSpec.coffee
@@ -0,0 +1,36 @@
+define [
+ 'underscore',
+ 'react',
+ 'jsx/shared/stores/ProgressStore'
+], (_, React, ProgressStore, I18n) ->
+ TestUtils = React.addons.TestUtils
+
+ module 'ProgressStoreSpec',
+ setup: ->
+ @progress_id = 1
+ @progress = {
+ id: @progress_id,
+ context_id: 1,
+ context_type: 'EpubExport',
+ user_id: 1,
+ tag: 'epub_export',
+ completion: 0,
+ workflow_state: 'queued'
+ }
+
+ @server = sinon.fakeServer.create()
+
+ teardown: ->
+ @server.restore()
+
+ test 'get', ->
+ @server.respondWith('GET', '/api/v1/progress/' + @progress_id, [
+ 200, {'Content-Type': 'application/json'},
+ JSON.stringify(@progress)
+ ])
+ ok _.isEmpty(ProgressStore.getState()), 'precondition'
+ ProgressStore.get(@progress_id)
+ @server.respond()
+
+ state = ProgressStore.getState()
+ deepEqual state[@progress.id], @progress
diff --git a/spec/controllers/epub_exports_controller_spec.rb b/spec/controllers/epub_exports_controller_spec.rb
new file mode 100644
index 00000000000..d8377b4ffe8
--- /dev/null
+++ b/spec/controllers/epub_exports_controller_spec.rb
@@ -0,0 +1,159 @@
+#
+# 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 .
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../apis/api_spec_helper')
+
+describe EpubExportsController do
+
+ before :once do
+ course_with_teacher(active_all: true)
+ student_in_course(active_all: true)
+ end
+
+ describe "GET index, format html" do
+ context "without user" do
+ it "should require user to be logged in to access the page" do
+ get 'index'
+ assert_unauthorized
+ end
+ end
+
+ context "with user" do
+ before(:once) do
+ user_session(@student)
+ @n = @student.courses.count
+ @n_more = 4
+ create_courses(@n_more, {
+ enroll_user: @student,
+ enrollment_type: 'StudentEnrollment'
+ })
+ @student.enrollments.last.update_attribute(
+ :workflow_state, 'completed'
+ )
+ end
+
+ it "should assign collection of courses and render" do
+ get :index
+
+ expect(response).to render_template(:index)
+ expect(response).to have_http_status(:success)
+ expect(assigns(:courses).size).to eq(@n + @n_more)
+ end
+ end
+ end
+
+ describe "GET :index.json", type: :request do
+ before(:once) do
+ @n = @student.courses.count
+ @n_more = 4
+ create_courses(@n_more, {
+ enroll_user: @student,
+ enrollment_type: 'StudentEnrollment'
+ })
+ @student.enrollments.last.update_attribute(
+ :workflow_state, 'completed'
+ )
+ end
+
+ it "should return course epub exports" do
+ json = api_call_as_user(@student, :get, "/api/v1/epub_exports", {
+ controller: :epub_exports,
+ action: :index,
+ format: 'json'
+ })
+
+ expect(json['courses'].size).to eq(@n + @n_more)
+ end
+ end
+
+ describe "GET :show.json", type: :request do
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student
+ })
+ end
+
+ it "should be success" do
+ json = api_call_as_user(@student, :get, "/api/v1/courses/#{@course.id}/epub_exports/#{epub_export.id}", {
+ controller: :epub_exports,
+ action: :show,
+ course_id: @course.to_param,
+ id: epub_export.to_param,
+ format: 'json'
+ })
+
+ expect(json['id']). to eq(@course.id)
+ expect(json['epub_export']['id']). to eq(epub_export.id)
+ end
+ end
+
+ describe "POST :create.json", type: :request do
+ before :each do
+ EpubExport.any_instance.stubs(:export).returns(true)
+ end
+
+ let_once(:url) do
+ "/api/v1/courses/#{@course.id}/epub_exports"
+ end
+
+ context "when epub_export doesn't exist" do
+ it "should return json with newly created epub_export" do
+ json = api_call_as_user(@student, :post, url, {
+ action: :create,
+ controller: :epub_exports,
+ course_id: @course.id,
+ format: 'json'
+ })
+
+ expect(json['epub_export']['workflow_state']).to eq('created')
+ end
+
+ it "should create one epub_export" do
+ expect {
+ api_call_as_user(@student, :post, url, {
+ action: :create,
+ controller: :epub_exports,
+ course_id: @course.id,
+ format: 'json'
+ })
+ }.to change{EpubExport.count}.from(0).to(1)
+ end
+ end
+
+ context "when there is a running epub_export" do
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student
+ })
+ end
+
+ it "should not create one epub_export" do
+ expect {
+ api_call_as_user(@student, :post, url, {
+ action: :create,
+ controller: :epub_exports,
+ course_id: @course.id,
+ format: 'json'
+ }, {}, {}, {
+ expected_status: 422
+ })
+ }.not_to change{EpubExport.count}
+ end
+ end
+ end
+end
diff --git a/spec/models/epub_export_spec.rb b/spec/models/epub_export_spec.rb
new file mode 100644
index 00000000000..5ee6fb7a1b7
--- /dev/null
+++ b/spec/models/epub_export_spec.rb
@@ -0,0 +1,194 @@
+#
+# 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 .
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe EpubExport do
+ before :once do
+ course_with_teacher(active_all: true)
+ student_in_course(active_all: true)
+ end
+
+ describe "after_create" do
+ it "should create one job progress" do
+ expect{@course.epub_exports.create(user: @student)}.to change{Progress.count}.from(0).to(1)
+ end
+ end
+
+ describe "#export" do
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student
+ })
+ end
+
+ context "method is successful" do
+ it "should create one content_export" do
+ expect{epub_export.export_without_send_later}.to change{ContentExport.count}.from(0).to(1)
+ end
+
+ it "should set state to 'exporting'" do
+ epub_export.export_without_send_later
+ expect(epub_export.workflow_state).to eq 'exporting'
+ end
+
+ it "should set job_progress completion to 25%" do
+ epub_export.export_without_send_later
+ expect(epub_export.job_progress.completion).to eq 25.0
+ end
+
+ it "should start job_progress" do
+ epub_export.export_without_send_later
+ expect(epub_export.job_progress.reload.running?).to be_truthy
+ end
+ end
+ end
+
+
+ describe "mark_exported" do
+ let_once(:content_export) do
+ @course.content_exports.create({
+ user: @student
+ })
+ end
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student,
+ content_export: content_export
+ })
+ end
+
+ context "when content export is successful" do
+ before(:once) do
+ epub_export.content_export.update_attribute(:workflow_state, 'exported')
+ epub_export.mark_exported_without_send_later
+ end
+
+ it "should change the workflow state of epub_export to exported" do
+ expect(epub_export.workflow_state).to eq 'exported'
+ end
+
+ it "should set job_progress completion to 50" do
+ expect(epub_export.job_progress.completion).to eq 50.0
+ end
+ end
+
+ context "when content export is failed" do
+ it "should change the workflow state of epub_export to failed" do
+ epub_export.content_export.update_attribute(:workflow_state, 'failed')
+ epub_export.mark_exported_without_send_later
+ expect(epub_export.workflow_state).to eq 'failed'
+ end
+ end
+ end
+
+ describe "#generate" do
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student
+ }).tap do |epub_export|
+ epub_export.update_attribute(:workflow_state, 'exported')
+ end
+ end
+
+ it "should set job_progress completion to 75" do
+ epub_export.generate_without_send_later
+ expect(epub_export.job_progress.completion).to eq 75.0
+ end
+
+ it "should set state to generating" do
+ epub_export.generate_without_send_later
+ expect(epub_export.generating?).to be_truthy
+ end
+ end
+
+ describe "permissions" do
+ describe ":create" do
+ context "when user can :read_as_admin" do
+ it "should be able to :create an epub export instance" do
+ expect(@course.grants_right?(@teacher, :read_as_admin)).to be_truthy, 'precondition'
+ expect(EpubExport.new(course: @course).grants_right?(@teacher, :create)).to be_truthy
+ end
+ end
+
+ context "when user can :participate_as_student" do
+ it "should be able to :create an epub export instance" do
+ expect(@course.grants_right?(@student, :participate_as_student)).to be_truthy, 'precondition'
+ expect(EpubExport.new(course: @course).grants_right?(@student, :create)).to be_truthy
+ end
+ end
+
+ context "when user cannot :participate_as_student" do
+ it "should not be able to :create an epub export" do
+ student_in_course
+ expect(@course.grants_right?(@student, :participate_as_student)).to be_falsey, 'precondition'
+ expect(EpubExport.new(course: @course).grants_right?(@student, :create)).to be_falsey
+ end
+ end
+ end
+
+ describe ":regenerate" do
+ let_once(:epub_export) do
+ @course.epub_exports.create(user: @student)
+ end
+
+ [ "generated", "failed" ].each do |state|
+ context "when state is #{state}" do
+ it "should allow regeneration" do
+ epub_export.update_attribute(:workflow_state, state)
+ expect(epub_export.grants_right?(@student, :regenerate)).to be_truthy
+ end
+ end
+ end
+ end
+ end
+
+ describe "scopes" do
+ let_once(:epub_export) do
+ @course.epub_exports.create({
+ user: @student
+ })
+ end
+
+ context "running" do
+ ['created', 'exporting', 'exported', 'generating'].each do |state|
+ it "should return epub export when workflow_state is #{state}" do
+ epub_export.update_attribute(:workflow_state, state)
+ expect(EpubExport.running.count).to eq 1
+ end
+ end
+
+ ['generated', 'failed', 'deleted'].each do |state|
+ it "should return epub export when workflow_state is #{state}" do
+ epub_export.update_attribute(:workflow_state, state)
+ expect(EpubExport.running.count).to eq 0
+ end
+ end
+ end
+
+ context "visible_to" do
+ it "should be visible to the user who created the epub export" do
+ expect(EpubExport.visible_to(@student.id).count).to eq 1
+ end
+
+ it "should not be visible to the user who didn't create the epub export" do
+ expect(EpubExport.visible_to(@teacher.id).count).to eq 0
+ end
+ end
+ end
+end
diff --git a/spec/models/epub_exports/create_service_spec.rb b/spec/models/epub_exports/create_service_spec.rb
new file mode 100644
index 00000000000..72fd4e962b1
--- /dev/null
+++ b/spec/models/epub_exports/create_service_spec.rb
@@ -0,0 +1,80 @@
+#
+# 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 .
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+describe EpubExports::CreateService do
+ before :once do
+ course_with_teacher(active_all: true)
+ student_in_course(active_all: true)
+ end
+
+ describe "#save" do
+ let_once(:create_service) do
+ EpubExports::CreateService.new(@course, @student)
+ end
+
+ it "should send save & export to epub_export" do
+ expect(create_service.epub_export.new_record?).to be_truthy, 'precondition'
+ create_service.epub_export.expects(:export).once.returns(nil)
+ expect(create_service.save).to be_truthy
+ expect(create_service.epub_export.new_record?).to be_falsey
+ end
+ end
+
+ describe "#epub_export" do
+ context "when user has an active epub_export" do
+ before(:once) do
+ @epub_export = @course.epub_exports.create(user: @student)
+ @epub_export.export_without_send_later
+ @service = EpubExports::CreateService.new(@course, @student)
+ end
+
+ it "should return said epub_export" do
+ expect(@service.epub_export).to eq @epub_export
+ end
+ end
+
+ context "when user has no active epub_exports" do
+ it "should return a new epub_export instance" do
+ service = EpubExports::CreateService.new(@course, @student)
+ expect(service.epub_export).to be_new_record
+ end
+ end
+ end
+
+ describe "#already_running?" do
+ context "when user has an active epub_export" do
+ before(:once) do
+ @course.epub_exports.create(user: @student).export_without_send_later
+ @service = EpubExports::CreateService.new(@course, @student)
+ end
+
+ it "should return true" do
+ expect(@service.already_running?).to be_truthy
+ end
+ end
+
+ context "when user doesn't have an active epub_export" do
+ it "should return true" do
+ service = EpubExports::CreateService.new(@course, @student)
+ expect(service.already_running?).to be_falsey
+ end
+ end
+ end
+end