Add real web zip exports to the downloads page

closes OFFW-47

test plan
- Load the Course Content Downloads page either via modules
  or going directly to /courses/:course_id/offline_web_exports
- Verify that there is a real list of downloads in oldest to
  newest order
- If there are any that are in process, they should not show up.
- A spinner should show up while loading the history
- If you can induce an error, you should get error text

Change-Id: I21cb0d0196fdd3fda93ee0680ff076e79b798780
Reviewed-on: https://gerrit.instructure.com/98236
Tested-by: Jenkins
Reviewed-by: Jon Willesen <jonw+gerrit@instructure.com>
QA-Review: Nathan Rogowski <nathan@instructure.com>
Product-Review: Mysti Sadler <mysti@instructure.com>
This commit is contained in:
Mysti Sadler 2016-12-20 17:11:42 -07:00
parent e1bf79985c
commit a30ab6da39
14 changed files with 194 additions and 111 deletions

View File

@ -118,7 +118,7 @@ class WebZipExportsController < ApplicationController
def index
return unless authorized_action(@context, @current_user, :read)
user_web_zips = @context.web_zip_exports.visible_to(@current_user)
user_web_zips = @context.web_zip_exports.visible_to(@current_user).order("created_at DESC")
web_zips_json = Api.paginate(user_web_zips, self, api_v1_web_zip_exports_url).map do |web_zip|
web_zip_export_json(web_zip)
end

View File

@ -1,27 +1,62 @@
define([
'react',
'axios',
'instructure-ui/Spinner',
'i18n!webzip_exports',
'compiled/str/splitAssetString',
'jsx/webzip_export/components/ExportList',
], (React, I18n, ExportList) => {
'jsx/webzip_export/components/Errors',
], (React, axios, {default: Spinner}, I18n, splitAssetString, ExportList, Errors) => {
class WebZipExportApp extends React.Component {
static datesAndLinksFromAPI (webZipExports) {
return webZipExports.filter(webZipExport =>
webZipExport.workflow_state === 'generated' || webZipExport.workflow_state === 'failed'
).map((webZipExport) => {
const url = webZipExport.zip_attachment ? webZipExport.zip_attachment.url : null
return {date: webZipExport.created_at, link: url}
}).reverse()
}
constructor (props) {
super(props)
this.state = {exports: this.fakeData()}
this.state = {exports: [], errors: []}
}
fakeData () {
return [
{date: 'Nov 11, 2016 @ 3:33 PM', link: 'https://example.com'},
{date: 'Nov 15, 2016 @ 7:07 PM', link: 'https://example.com'}
]
componentDidMount () {
const courseId = splitAssetString(ENV.context_asset_string)[1]
this.loadExistingExports(courseId)
}
loadExistingExports (courseId) {
axios.get(`/api/v1/courses/${courseId}/web_zip_exports`)
.then((response) => {
this.setState({
exports: WebZipExportApp.datesAndLinksFromAPI(response.data),
errors: [],
})
})
.catch((response) => {
this.setState({
exports: [],
errors: [response],
})
})
}
render () {
let app = null
if (this.state.exports.length === 0 && this.state.errors.length === 0) {
app = (<Spinner size="small" title={I18n.t('Loading')} />)
} else if (this.state.exports.length === 0) {
app = (<Errors errors={this.state.errors} />)
} else {
app = (<ExportList exports={this.state.exports} />)
}
return (
<div>
<h1>{I18n.t('Course Content Downloads')}</h1>
<ExportList exports={this.state.exports} />
{app}
</div>
)
}

View File

@ -1,18 +0,0 @@
define([
'redux-actions',
], (ReduxActions) => {
const { createAction } = ReduxActions
const keys = {
CREATE_NEW_EXPORT: 'CREATE_NEW_EXPORT'
}
const actions = {
createNewExport: createAction(keys.CREATE_NEW_EXPORT)
}
return {
actions,
keys
}
})

View File

@ -0,0 +1,14 @@
define([
'react',
'i18n!webzip_exports',
], (React, I18n) => {
const Errors = (props) => {
return (
<p className="webzipexport__errors">
{I18n.t('An error occurred. Please try again later.')}
</p>
)
}
return Errors
})

View File

@ -4,13 +4,16 @@ define([
], (React, ExportListItem) => {
class ExportList extends React.Component {
static propTypes = {
exports: React.PropTypes.array.isRequired
exports: React.PropTypes.arrayOf(React.PropTypes.shape({
date: React.PropTypes.string.isRequired,
link: React.PropTypes.string.isRequired,
})).isRequired
}
renderExportListItems () {
return this.props.exports.map(function (webzip, key) {
return <ExportListItem key={key} link={webzip.link} date={webzip.date} />
})
return this.props.exports.map((webzip, key) =>
<ExportListItem key={key} link={webzip.link} date={webzip.date} />
)
}
render () {

View File

@ -3,7 +3,9 @@ define([
'underscore',
'jsx/shared/ApiProgressBar',
'i18n!webzip_exports',
], (React, _, ApiProgressBar, I18n) => {
'jquery',
'jquery.instructure_date_and_time'
], (React, _, ApiProgressBar, I18n, $) => {
class ExportListItem extends React.Component {
static propTypes = {
date: React.PropTypes.string.isRequired,
@ -14,7 +16,7 @@ define([
return (
<li className={'webzipexport__list__item'}>
<span>{I18n.t('Course Content Download from')}</span>
<span>: <a href={this.props.link}>{this.props.date}</a></span>
<span>: <a href={this.props.link}>{$.datetimeString(this.props.date)}</a></span>
</li>
)
}

View File

@ -1,17 +0,0 @@
define([
'redux-actions',
'./actions',
], (ReduxActions, Actions) => {
const { handleActions } = ReduxActions
const reducer = handleActions({
[Actions.keys.CREATE_NEW_EXPORT]: (state = {}, action) => ({
exports: [
...state.exports,
{date: action.payload.date, link: action.payload.link}
]
})
})
return reducer
})

View File

@ -1,19 +0,0 @@
define([
'redux',
'redux-thunk',
'redux-logger',
'../reducer',
], (Redux, {default: ReduxThunk}, reduxLogger, reducer) => {
const { createStore, applyMiddleware } = Redux
const logger = reduxLogger()
const createStoreWithMiddleware = applyMiddleware(
logger,
ReduxThunk
)(createStore)
return function configureStore (state = {}) {
return createStoreWithMiddleware(reducer, state)
}
})

View File

@ -0,0 +1,102 @@
define([
'react',
'enzyme',
'moxios',
'jsx/webzip_export/App',
], (React, enzyme, moxios, WebZipExportApp) => {
module('WebZip Export App', {
setup () {
moxios.install()
},
teardown () {
moxios.uninstall()
}
})
test('renders a spinner before API call', () => {
const wrapper = enzyme.shallow(<WebZipExportApp />)
const node = wrapper.find('Spinner')
ok(node.exists())
})
test('renders a list of webzip exports', (assert) => {
const done = assert.async()
const data = [{
created_at: '1776-12-25T22:00:00Z',
zip_attachment: {url: 'http://example.com/washingtoncrossingdelaware'},
workflow_state: 'generated',
}]
moxios.stubRequest('/api/v1/courses/2/web_zip_exports', {
status: 200,
responseText: data
})
ENV.context_asset_string = 'courses_2'
const wrapper = enzyme.shallow(<WebZipExportApp />)
wrapper.instance().componentDidMount()
moxios.wait(() => {
const node = wrapper.find('ExportList')
ok(node.exists())
done()
})
})
test('renders errors', (assert) => {
const done = assert.async()
moxios.stubRequest('/api/v1/courses/2/web_zip_exports', {
status: 666,
responseText: 'Demons!'
})
ENV.context_asset_string = 'courses_2'
const wrapper = enzyme.shallow(<WebZipExportApp />)
wrapper.instance().componentDidMount()
moxios.wait(() => {
const node = wrapper.find('Errors')
ok(node.exists())
done()
})
})
module('datesAndLinksFromAPI')
test('returns a JS object with webzip created_at dates and webzip export attachment urls', () => {
const data = [{
created_at: '2017-01-03T15:55Z',
zip_attachment: {url: 'http://example.com/stuff'},
workflow_state: 'generated',
},
{
created_at: '1776-12-25T22:00Z',
zip_attachment: {url: 'http://example.com/washingtoncrossingdelaware'},
workflow_state: 'generated',
}]
const formatted = WebZipExportApp.datesAndLinksFromAPI(data)
const expected = [{
date: '1776-12-25T22:00Z',
link: 'http://example.com/washingtoncrossingdelaware'
},
{
date: '2017-01-03T15:55Z',
link: 'http://example.com/stuff'
}]
deepEqual(formatted, expected)
})
test('does not include items with a workflow_state other than generated', () => {
const data = [{
created_at: '2017-01-03T15:55Z',
zip_attachment: {url: 'http://example.com/stuff'},
workflow_state: 'generating',
},
{
created_at: '1776-12-25T22:00Z',
zip_attachment: {url: 'http://example.com/washingtoncrossingdelaware'},
workflow_state: 'generated',
}]
const formatted = WebZipExportApp.datesAndLinksFromAPI(data)
const expected = [{
date: '1776-12-25T22:00Z',
link: 'http://example.com/washingtoncrossingdelaware'
}]
deepEqual(formatted, expected)
})
})

View File

@ -1,16 +0,0 @@
define([
'jsx/webzip_export/actions',
], (Actions) => {
module('WebZip Export Actions');
test('createNewExport returns the proper action', () => {
const payload = {date: 'July 20, 1969 @ 20:18 UTC', link: 'http://example.com/manonthemoon'}
const actual = Actions.actions.createNewExport(payload);
const expected = {
type: 'CREATE_NEW_EXPORT',
payload
};
deepEqual(actual, expected);
})
})

View File

@ -0,0 +1,15 @@
define([
'react',
'react-dom',
'enzyme',
'jsx/webzip_export/components/Errors',
], (React, ReactDOM, enzyme, Errors) => {
module('Web Zip Export Errors')
test('renders the Error component', () => {
const errors = [{response: 'Instance of demon found in code', code: 666}];
const tree = enzyme.shallow(<Errors errors={errors} />)
const node = tree.find('.webzipexport__errors')
ok(node.exists())
})
})

View File

@ -7,7 +7,7 @@ define([
module('ExportListItem')
test('renders the ExportListItem component', () => {
const date = 'Sept 11, 2001 @ 8:46 AM'
const date = 'Sept 11, 2001 at 8:46am'
const link = 'https://example.com/neverforget'
const tree = TestUtils.renderIntoDocument(<ExportListItem date={date} link={link} />)

View File

@ -1,19 +1,19 @@
define([
'react',
'react-dom',
'react-addons-test-utils',
'enzyme',
'jsx/webzip_export/components/ExportList',
], (React, ReactDOM, TestUtils, ExportList) => {
], (React, ReactDOM, enzyme, ExportList) => {
module('ExportList')
test('renders the ExportList component', () => {
const exports = [
{date: 'July 4, 1776 @ 3:33 PM', link: 'https://example.com/declarationofindependence'},
{date: 'Nov 9, 1989 @ 9:00 AM', link: 'https://example.com/berlinwallfalls'}
{date: 'July 4, 1776 at 3:33pm', link: 'https://example.com/declarationofindependence'},
{date: 'Nov 9, 1989 at 9am', link: 'https://example.com/berlinwallfalls'}
]
const tree = TestUtils.renderIntoDocument(<ExportList exports={exports} />)
const ExportListComponent = TestUtils.findRenderedDOMComponentWithClass(tree, 'webzipexport__list')
ok(ExportListComponent)
const tree = enzyme.shallow(<ExportList exports={exports} />)
const node = tree.find('.webzipexport__list')
ok(node.exists())
})
})

View File

@ -1,18 +0,0 @@
define([
'jsx/webzip_export/reducer'
], (reducer) => {
module('Webzip Exports Reducer')
test('creates new export on CREATE_NEW_EXPORT', () => {
const initialState = {
exports: [{date: 'December 7, 1941 @ 8:00 AM', link: 'http://example.com/pearlharbor'}]
}
const newState = reducer(initialState, {
type: 'CREATE_NEW_EXPORT',
payload: {date: 'December 25, 1776 @ 10:00 PM', link: 'http://example.com/washingtoncrossingdelaware'}
})
equal(newState.exports.length, 2)
})
})