diff --git a/src/components/SubmissionFailures/FailuresList/FailuresList.js b/src/components/SubmissionFailures/FailuresList/FailuresList.js
new file mode 100644
index 000000000..0075e9b16
--- /dev/null
+++ b/src/components/SubmissionFailures/FailuresList/FailuresList.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Table } from 'react-bootstrap';
+import { FormattedMessage } from 'react-intl';
+import FailuresListItem from '../FailuresListItem';
+
+const FailuresList = ({ failures, createActions }) =>
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+
+ {failures.map((failure, i) =>
+
+ )}
+
+ {failures.length === 0 &&
+
+
+
+ |
+
}
+
+
;
+
+FailuresList.propTypes = {
+ failures: PropTypes.array,
+ createActions: PropTypes.func,
+ failure: PropTypes.object
+};
+
+export default FailuresList;
diff --git a/src/components/SubmissionFailures/FailuresList/index.js b/src/components/SubmissionFailures/FailuresList/index.js
new file mode 100644
index 000000000..5290225a3
--- /dev/null
+++ b/src/components/SubmissionFailures/FailuresList/index.js
@@ -0,0 +1 @@
+export default from './FailuresList';
diff --git a/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js
new file mode 100644
index 000000000..c4b01c858
--- /dev/null
+++ b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedDate, FormattedTime } from 'react-intl';
+import Icon from 'react-fontawesome';
+import { OverlayTrigger, Tooltip } from 'react-bootstrap';
+
+const FailuresListItem = ({ id, createActions, failure }) =>
+
+
+
+ {failure.type}
+
+ }
+ >
+
+ {failure.type === 'broker_reject' && }
+ {failure.type === 'evaluation_failure' && }
+ {failure.type === 'loading_failure' && }
+
+
+ |
+
+ {failure.description}
+ |
+
+
+ {', '}
+
+ |
+
+ {failure.resolvedAt
+ ?
+
+ {', '}
+
+
+ : —}
+ |
+
+ {failure.resolutionNote
+ ?
+ {failure.resolutionNote}
+
+ : —}
+ |
+
+ {createActions && createActions(id)}
+ |
+
;
+
+FailuresListItem.propTypes = {
+ id: PropTypes.string.isRequired,
+ failure: PropTypes.object.isRequired,
+ createActions: PropTypes.func
+};
+
+export default FailuresListItem;
diff --git a/src/components/SubmissionFailures/FailuresListItem/index.js b/src/components/SubmissionFailures/FailuresListItem/index.js
new file mode 100644
index 000000000..87ad69694
--- /dev/null
+++ b/src/components/SubmissionFailures/FailuresListItem/index.js
@@ -0,0 +1 @@
+export default from './FailuresListItem';
diff --git a/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js b/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js
new file mode 100644
index 000000000..ac6a4eeef
--- /dev/null
+++ b/src/components/SubmissionFailures/ResolveFailure/ResolveFailure.js
@@ -0,0 +1,105 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+import { CloseIcon } from '../../icons';
+import { FormattedMessage } from 'react-intl';
+import { Field, reduxForm } from 'redux-form';
+import TextField from '../../forms/Fields/TextField';
+import SubmitButton from '../../forms/SubmitButton';
+
+const maxNoteLength = value =>
+ value && value.length >= 255
+ ?
+ : undefined;
+
+const ResolveFailure = ({
+ isOpen,
+ onClose,
+ submitting,
+ handleSubmit,
+ anyTouched,
+ submitFailed = false,
+ submitSucceeded = false,
+ invalid,
+ reset
+}) =>
+
+
+
+
+
+
+
+
+ }
+ validate={maxNoteLength}
+ />
+
+
+ handleSubmit(data).then(() => reset())}
+ submitting={submitting}
+ dirty={anyTouched}
+ hasSucceeded={submitSucceeded}
+ hasFailed={submitFailed}
+ invalid={invalid}
+ messages={{
+ submit: (
+
+ ),
+ submitting: (
+
+ ),
+ success: (
+
+ )
+ }}
+ />
+
+
+
+ ;
+
+ResolveFailure.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ submitFailed: PropTypes.bool,
+ anyTouched: PropTypes.bool,
+ submitSucceeded: PropTypes.bool,
+ submitting: PropTypes.bool,
+ invalid: PropTypes.bool,
+ reset: PropTypes.func,
+ onClose: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool.isRequired
+};
+
+export default reduxForm({ form: 'resolve-failure' })(ResolveFailure);
diff --git a/src/components/SubmissionFailures/ResolveFailure/index.js b/src/components/SubmissionFailures/ResolveFailure/index.js
new file mode 100644
index 000000000..6d3c96060
--- /dev/null
+++ b/src/components/SubmissionFailures/ResolveFailure/index.js
@@ -0,0 +1 @@
+export default from './ResolveFailure';
diff --git a/src/components/layout/Sidebar/Admin.js b/src/components/layout/Sidebar/Admin.js
index 65d835bd3..d59a05c70 100644
--- a/src/components/layout/Sidebar/Admin.js
+++ b/src/components/layout/Sidebar/Admin.js
@@ -7,7 +7,10 @@ import MenuItem from '../../widgets/Sidebar/MenuItem';
import withLinks from '../../../hoc/withLinks';
-const Admin = ({ currentUrl, links: { ADMIN_INSTANCES_URI, USERS_URI } }) => (
+const Admin = ({
+ currentUrl,
+ links: { ADMIN_INSTANCES_URI, USERS_URI, FAILURES_URI }
+}) =>
(
currentPath={currentUrl}
link={USERS_URI}
/>
-
-);
+
+ }
+ currentPath={currentUrl}
+ link={FAILURES_URI}
+ />
+ ;
Admin.propTypes = {
currentUrl: PropTypes.string.isRequired,
diff --git a/src/links/index.js b/src/links/index.js
index 3ed26496b..ea5b3648a 100644
--- a/src/links/index.js
+++ b/src/links/index.js
@@ -82,6 +82,9 @@ export const linksFactory = lang => {
const ADMIN_EDIT_INSTANCE_URI_FACTORY = instanceId =>
`${ADMIN_INSTANCES_URI}/${instanceId}/edit`;
+ // failures details
+ const FAILURES_URI = `${prefix}/app/submission-failures`;
+
// download files
const DOWNLOAD = fileId => `${API_BASE}/uploaded-files/${fileId}/download`;
@@ -127,7 +130,8 @@ export const linksFactory = lang => {
ADMIN_EDIT_INSTANCE_URI_FACTORY,
DOWNLOAD,
REFERENCE_SOLUTION_EVALUATION_URI_FACTORY,
- LOGIN_EXTERN_FINALIZATION
+ LOGIN_EXTERN_FINALIZATION,
+ FAILURES_URI
};
};
diff --git a/src/pages/SubmissionFailures/SubmissionFailures.js b/src/pages/SubmissionFailures/SubmissionFailures.js
new file mode 100644
index 000000000..b7fba8813
--- /dev/null
+++ b/src/pages/SubmissionFailures/SubmissionFailures.js
@@ -0,0 +1,156 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+
+import PageContent from '../../components/layout/PageContent';
+import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer';
+import {
+ fetchAllFailures,
+ resolveFailure
+} from '../../redux/modules/submissionFailures';
+import {
+ fetchManyStatus,
+ readySubmissionFailuresSelector
+} from '../../redux/selectors/submissionFailures';
+import FailuresList from '../../components/SubmissionFailures/FailuresList/FailuresList';
+import Box from '../../components/widgets/Box/Box';
+import ResolveFailure from '../../components/SubmissionFailures/ResolveFailure/ResolveFailure';
+import { Button } from 'react-bootstrap';
+
+class SubmissionFailures extends Component {
+ state = { isOpen: false, activeId: null };
+
+ static loadAsync = (params, dispatch) =>
+ Promise.all([dispatch(fetchAllFailures)]);
+
+ componentWillMount() {
+ this.props.loadAsync();
+ }
+
+ render() {
+ const { submissionFailures, fetchStatus, resolveFailure } = this.props;
+
+ return (
+
+ }
+ description={
+
+ }
+ />
+ }
+ failed={
+
+ }
+ description={
+
+ }
+ />
+ }
+ >
+ {() =>
+
+ }
+ description={
+
+ }
+ breadcrumbs={[
+ {
+ text: (
+
+ ),
+ iconName: 'fort-awesome'
+ }
+ ]}
+ >
+
+ }
+ unlimitedHeight
+ noPadding
+ >
+
+
+ }
+ />
+
+ this.setState({ isOpen: false, activeId: null })}
+ onSubmit={data =>
+ resolveFailure(this.state.activeId, data.note).then(() =>
+ this.setState({ isOpen: false, activeId: null })
+ )}
+ />
+
+
+ }
+
+ );
+ }
+}
+
+SubmissionFailures.propTypes = {
+ loadAsync: PropTypes.func.isRequired,
+ fetchStatus: PropTypes.string,
+ submissionFailures: PropTypes.array,
+ resolveFailure: PropTypes.func
+};
+
+export default connect(
+ state => ({
+ fetchStatus: fetchManyStatus(state),
+ submissionFailures: readySubmissionFailuresSelector(state)
+ }),
+ dispatch => ({
+ loadAsync: () => SubmissionFailures.loadAsync({}, dispatch),
+ resolveFailure: (id, note) => dispatch(resolveFailure(id, note))
+ })
+)(SubmissionFailures);
diff --git a/src/pages/SubmissionFailures/index.js b/src/pages/SubmissionFailures/index.js
new file mode 100644
index 000000000..8f4a46d73
--- /dev/null
+++ b/src/pages/SubmissionFailures/index.js
@@ -0,0 +1 @@
+export default from './SubmissionFailures';
diff --git a/src/pages/routes.js b/src/pages/routes.js
index 5336beb4d..0de0f3f4d 100644
--- a/src/pages/routes.js
+++ b/src/pages/routes.js
@@ -38,6 +38,7 @@ import Pipelines from './Pipelines';
import EditPipeline from './EditPipeline';
import Pipeline from './Pipeline';
import FAQ from './FAQ';
+import SubmissionFailures from './SubmissionFailures/SubmissionFailures';
import ChangePassword from './ChangePassword';
import ResetPassword from './ResetPassword';
@@ -137,6 +138,9 @@ const createRoutes = getState => {
+
+
+
diff --git a/src/redux/modules/submissionFailures.js b/src/redux/modules/submissionFailures.js
new file mode 100644
index 000000000..db262bae8
--- /dev/null
+++ b/src/redux/modules/submissionFailures.js
@@ -0,0 +1,46 @@
+import { handleActions } from 'redux-actions';
+import factory, { initialState } from '../helpers/resourceManager';
+import { createApiAction } from '../middleware/apiMiddleware';
+import { fromJS } from 'immutable';
+
+const resourceName = 'submissionFailures';
+var { actions, reduceActions } = factory({ resourceName });
+
+export const additionalActionTypes = {
+ RESOLVE: 'recodex/submissionFailures/RESOLVE',
+ RESOLVE_FULFILLED: 'recodex/submissionFailures/RESOLVE_FULFILLED'
+};
+
+/**
+ * Actions
+ */
+export const fetchManyEndpoint = '/submission-failures';
+
+export const fetchAllFailures = actions.fetchMany({
+ endpoint: fetchManyEndpoint
+});
+
+export const resolveFailure = (id, note) =>
+ createApiAction({
+ type: additionalActionTypes.RESOLVE,
+ method: 'POST',
+ endpoint: `/submission-failures/${id}/resolve`,
+ body: { note },
+ meta: { id }
+ });
+
+/**
+ * Reducer takes mainly care about all the state of individual attachments
+ */
+
+const reducer = handleActions(
+ Object.assign({}, reduceActions, {
+ [additionalActionTypes.RESOLVE_FULFILLED]: (
+ state,
+ { meta: { id }, payload }
+ ) => state.setIn(['resources', id, 'data'], fromJS(payload))
+ }),
+ initialState
+);
+
+export default reducer;
diff --git a/src/redux/reducer.js b/src/redux/reducer.js
index 18cda1b42..0300459ec 100644
--- a/src/redux/reducer.js
+++ b/src/redux/reducer.js
@@ -46,6 +46,7 @@ import sisSupervisedCourses from './modules/sisSupervisedCourses';
import sisPossibleParents from './modules/sisPossibleParents';
import referenceSolutionEvaluations from './modules/referenceSolutionEvaluations';
import submissionEvaluations from './modules/submissionEvaluations';
+import submissionFailures from './modules/submissionFailures';
const createRecodexReducers = token => ({
auth: auth(token),
@@ -91,7 +92,8 @@ const createRecodexReducers = token => ({
sisSubscribedGroups,
sisSupervisedCourses,
sisPossibleParents,
- submissionEvaluations
+ submissionEvaluations,
+ submissionFailures
});
const librariesReducers = {
diff --git a/src/redux/selectors/submissionFailures.js b/src/redux/selectors/submissionFailures.js
new file mode 100644
index 000000000..64d02bf16
--- /dev/null
+++ b/src/redux/selectors/submissionFailures.js
@@ -0,0 +1,31 @@
+import { createSelector } from 'reselect';
+import { fetchManyEndpoint } from '../modules/submissionFailures';
+import { isReady, getJsData } from '../helpers/resourceManager';
+
+const getSubmissionFailures = state => state.submissionFailures;
+const getResources = failures => failures.get('resources');
+
+export const submissionFailuresSelector = createSelector(
+ getSubmissionFailures,
+ getResources
+);
+
+export const fetchManyStatus = createSelector(getSubmissionFailures, state =>
+ state.getIn(['fetchManyStatus', fetchManyEndpoint])
+);
+
+export const getSubmissionFailure = failureId =>
+ createSelector(submissionFailuresSelector, failures =>
+ failures.get(failureId)
+ );
+
+export const readySubmissionFailuresSelector = createSelector(
+ submissionFailuresSelector,
+ failures =>
+ failures
+ .toList()
+ .filter(isReady)
+ .map(getJsData)
+ .sort((a, b) => b.createdAt - a.createdAt)
+ .toArray()
+);