-
- {data.parentGroupsIds.map(
- (groupId, i) =>
- i !== 0 &&
-
- {' '}
- /
-
- )}
-
-
-
- {data.parentGroupsIds.length > 1 &&
}
+
+
{(isAdmin || isSuperAdmin) &&
@@ -327,7 +317,7 @@ const mapStateToProps = (state, { params: { groupId } }) => {
instance: isReady(group)
? instanceSelector(state, groupData.instanceId)
: null,
- groups: groupsSelectors(state),
+ groups: groupsSelector(state),
publicGroups: publicGroupsSelectors(state),
publicAssignments: groupsAssignmentsSelector(groupId, 'public')(state),
allAssignments: groupsAssignmentsSelector(groupId, 'all')(state),
diff --git a/src/pages/Pipeline/Pipeline.js b/src/pages/Pipeline/Pipeline.js
index b95245566..97966f5ef 100644
--- a/src/pages/Pipeline/Pipeline.js
+++ b/src/pages/Pipeline/Pipeline.js
@@ -103,32 +103,30 @@ class Pipeline extends Component {
>
{pipeline =>
+
+
+ {isAuthorOfPipeline(pipeline.id) &&
+
+
+
+
+
+
+ }
+ forkPipeline(forkId, formData)}
+ />
+
+
+
-
-
- {isAuthorOfPipeline(pipeline.id) &&
-
-
-
-
-
-
- }
- forkPipeline(forkId, formData)}
- />
-
-
-
diff --git a/src/pages/ReferenceSolution/ReferenceSolution.js b/src/pages/ReferenceSolution/ReferenceSolution.js
index 6371744a1..36c6a9787 100644
--- a/src/pages/ReferenceSolution/ReferenceSolution.js
+++ b/src/pages/ReferenceSolution/ReferenceSolution.js
@@ -12,7 +12,6 @@ import { Row, Col, Button } from 'react-bootstrap';
import withLinks from '../../hoc/withLinks';
import Page from '../../components/layout/Page';
-import { clientOnly } from '../../helpers/clientOnly';
import {
fetchReferenceSolutionsIfNeeded,
@@ -212,8 +211,7 @@ export default withLinks(
referenceSolutions: referenceSolutionsSelector(exerciseId)(state)
}),
(dispatch, { params }) => ({
- loadAsync: () =>
- clientOnly(() => ReferenceSolution.loadAsync(params, dispatch)),
+ loadAsync: () => ReferenceSolution.loadAsync(params, dispatch),
refreshSolutionEvaluations: () => {
dispatch(fetchReferenceSolutions(params.exerciseId));
},
diff --git a/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js b/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js
index cc0ea5fd7..165bbe427 100644
--- a/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js
+++ b/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js
@@ -11,7 +11,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import withLinks from '../../hoc/withLinks';
import Page from '../../components/layout/Page';
-import { clientOnly } from '../../helpers/clientOnly';
import { fetchReferenceSolutionsIfNeeded } from '../../redux/modules/referenceSolutions';
import { referenceSolutionsSelector } from '../../redux/selectors/referenceSolutions';
@@ -162,9 +161,7 @@ export default withLinks(
}),
(dispatch, { params }) => ({
loadAsync: () =>
- clientOnly(() =>
- ReferenceSolutionEvaluation.loadAsync(params, dispatch)
- ),
+ ReferenceSolutionEvaluation.loadAsync(params, dispatch),
downloadEvaluationArchive: e => {
e.preventDefault();
dispatch(downloadEvaluationArchive(params.evaluationId));
diff --git a/src/pages/Submission/Submission.js b/src/pages/Submission/Submission.js
index a2eca7c84..5cc2aac8f 100644
--- a/src/pages/Submission/Submission.js
+++ b/src/pages/Submission/Submission.js
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
import Page from '../../components/layout/Page';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
@@ -10,6 +10,7 @@ import SubmissionDetail, {
} from '../../components/Submissions/SubmissionDetail';
import AcceptSolutionContainer from '../../containers/AcceptSolutionContainer';
import ResubmitSolutionContainer from '../../containers/ResubmitSolutionContainer';
+import HierarchyLineContainer from '../../containers/HierarchyLineContainer';
import { fetchGroupsStats } from '../../redux/modules/stats';
import { fetchAssignmentIfNeeded } from '../../redux/modules/assignments';
@@ -22,7 +23,7 @@ import {
isSuperAdmin
} from '../../redux/selectors/users';
import { loggedInUserIdSelector } from '../../redux/selectors/auth';
-import { clientOnly } from '../../helpers/clientOnly';
+import { getLocalizedName } from '../../helpers/getLocalizedData';
class Submission extends Component {
static loadAsync = ({ submissionId, assignmentId }, dispatch) =>
@@ -48,13 +49,14 @@ class Submission extends Component {
assignment,
submission,
params: { assignmentId },
- isSupervisorOrMore
+ isSupervisorOrMore,
+ intl: { locale }
} = this.props;
return (
assignment.name}
+ title={assignment => getLocalizedName(assignment, locale)}
description={
{(submission, assignment) =>
+
{isSupervisorOrMore(assignment.groupId) &&
@@ -133,19 +136,22 @@ Submission.propTypes = {
children: PropTypes.element,
submission: PropTypes.object,
loadAsync: PropTypes.func.isRequired,
- isSupervisorOrMore: PropTypes.func.isRequired
+ isSupervisorOrMore: PropTypes.func.isRequired,
+ intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired
};
-export default connect(
- (state, { params: { submissionId, assignmentId } }) => ({
- submission: getSubmission(submissionId)(state),
- assignment: getAssignment(assignmentId)(state),
- isSupervisorOrMore: groupId =>
- isSupervisorOf(loggedInUserIdSelector(state), groupId)(state) ||
- isAdminOf(loggedInUserIdSelector(state), groupId)(state) ||
- isSuperAdmin(loggedInUserIdSelector(state))(state)
- }),
- (dispatch, { params }) => ({
- loadAsync: () => clientOnly(() => Submission.loadAsync(params, dispatch))
- })
-)(Submission);
+export default injectIntl(
+ connect(
+ (state, { params: { submissionId, assignmentId } }) => ({
+ submission: getSubmission(submissionId)(state),
+ assignment: getAssignment(assignmentId)(state),
+ isSupervisorOrMore: groupId =>
+ isSupervisorOf(loggedInUserIdSelector(state), groupId)(state) ||
+ isAdminOf(loggedInUserIdSelector(state), groupId)(state) ||
+ isSuperAdmin(loggedInUserIdSelector(state))(state)
+ }),
+ (dispatch, { params }) => ({
+ loadAsync: () => Submission.loadAsync(params, dispatch)
+ })
+ )(Submission)
+);
diff --git a/src/redux/middleware/loggerMiddleware.js b/src/redux/middleware/loggerMiddleware.js
new file mode 100644
index 000000000..90c982d7f
--- /dev/null
+++ b/src/redux/middleware/loggerMiddleware.js
@@ -0,0 +1,20 @@
+const middleware = store => next => action => {
+ let verbose = false;
+ if (verbose) {
+ console.log(action);
+ } else {
+ console.log(action.type);
+ }
+
+ let res = next(action);
+
+ if (verbose) {
+ console.log('State After Actions:');
+ console.log('--------------------');
+ console.log(store.getState().assignments);
+ }
+
+ return res;
+};
+
+export default middleware;
diff --git a/src/redux/modules/exerciseScoreConfig.js b/src/redux/modules/exerciseScoreConfig.js
new file mode 100644
index 000000000..204230209
--- /dev/null
+++ b/src/redux/modules/exerciseScoreConfig.js
@@ -0,0 +1,19 @@
+import { handleActions } from 'redux-actions';
+import factory, { initialState } from '../helpers/resourceManager';
+
+/**
+ * Create actions & reducer
+ */
+
+const resourceName = 'exerciseScoreConfig';
+const { actions, reduceActions } = factory({
+ resourceName,
+ apiEndpointFactory: id => `/exercises/${id}/score-config`
+});
+
+export const fetchScoreConfig = actions.fetchResource;
+export const fetchScoreConfigIfNeeded = actions.fetchOneIfNeeded;
+export const setScoreConfig = actions.updateResource;
+
+const reducer = handleActions(reduceActions, initialState);
+export default reducer;
diff --git a/src/redux/modules/exercises.js b/src/redux/modules/exercises.js
index 6fc69ccaa..67cbe8857 100644
--- a/src/redux/modules/exercises.js
+++ b/src/redux/modules/exercises.js
@@ -48,13 +48,18 @@ export const forkStatuses = {
FULFILLED: 'FULFILLED'
};
-export const forkExercise = (id, forkId) =>
- createApiAction({
+export const forkExercise = (id, forkId, formData = null) => {
+ let actionData = {
type: additionalActionTypes.FORK_EXERCISE,
endpoint: `/exercises/${id}/fork`,
method: 'POST',
meta: { id, forkId }
- });
+ };
+ if (formData && formData.groupId) {
+ actionData.body = { groupId: formData.groupId };
+ }
+ return createApiAction(actionData);
+};
export const create = actions.addResource;
export const editExercise = actions.updateResource;
diff --git a/src/redux/modules/files.js b/src/redux/modules/files.js
index 2fe362128..06cc1d039 100644
--- a/src/redux/modules/files.js
+++ b/src/redux/modules/files.js
@@ -25,14 +25,14 @@ export const download = downloadHelper({
fetch: fetchFileIfNeeded,
actionType: actionTypes.DOWNLOAD,
fileNameSelector: (id, state) => getJsData(getFile(id)(state)).name,
- contentType: 'text/plain;charset=utf-8'
+ contentType: 'application/octet-stream'
});
export const downloadSupplementaryFile = downloadHelper({
endpoint: id => `/uploaded-files/supplementary-file/${id}/download`,
fetch: fetchFileIfNeeded,
actionType: actionTypes.DOWNLOAD,
fileNameSelector: (id, state) => getJsData(getFile(id)(state)).name,
- contentType: 'text/plain;charset=utf-8'
+ contentType: 'application/octet-stream'
});
/**
diff --git a/src/redux/modules/pipelines.js b/src/redux/modules/pipelines.js
index f5102fc9f..e0e88527b 100644
--- a/src/redux/modules/pipelines.js
+++ b/src/redux/modules/pipelines.js
@@ -37,19 +37,15 @@ export const forkStatuses = {
FULFILLED: 'FULFILLED'
};
-export const forkPipeline = (id, forkId, exerciseId = null) => {
+export const forkPipeline = (id, forkId, formData = null) => {
let actionData = {
type: additionalActionTypes.FORK_PIPELINE,
endpoint: `/pipelines/${id}/fork`,
method: 'POST',
meta: { id, forkId }
};
- if (
- Object.keys(exerciseId).length !== 0 &&
- exerciseId.constructor === Object &&
- exerciseId !== null
- ) {
- actionData.body = { exerciseId };
+ if (formData && formData.exerciseId) {
+ actionData.body = { exerciseId: formData.exerciseId };
}
return createApiAction(actionData);
};
diff --git a/src/redux/reducer.js b/src/redux/reducer.js
index 8f83dd6c5..e5966d26b 100644
--- a/src/redux/reducer.js
+++ b/src/redux/reducer.js
@@ -11,6 +11,7 @@ import emailVerification from './modules/emailVerification';
import evaluationProgress from './modules/evaluationProgress';
import exerciseConfigs from './modules/exerciseConfigs';
import exerciseEnvironmentConfigs from './modules/exerciseEnvironmentConfigs';
+import exerciseScoreConfig from './modules/exerciseScoreConfig';
import exercises from './modules/exercises';
import pipelines from './modules/pipelines';
import files from './modules/files';
@@ -56,6 +57,7 @@ const createRecodexReducers = token => ({
evaluationProgress,
exerciseConfigs,
exerciseEnvironmentConfigs,
+ exerciseScoreConfig,
exercises,
pipelines,
files,
diff --git a/src/redux/selectors/exerciseScoreConfig.js b/src/redux/selectors/exerciseScoreConfig.js
new file mode 100644
index 000000000..6a3881eaa
--- /dev/null
+++ b/src/redux/selectors/exerciseScoreConfig.js
@@ -0,0 +1,14 @@
+import { createSelector } from 'reselect';
+
+const getExerciseScoreConfig = state => state.exerciseScoreConfig;
+const getResources = exerciseScoreConfig =>
+ exerciseScoreConfig.get('resources');
+
+export const exerciseScoreConfigsSelector = createSelector(
+ getExerciseScoreConfig,
+ getResources
+);
+export const exerciseScoreConfigSelector = exerciseId =>
+ createSelector(exerciseScoreConfigsSelector, configs =>
+ configs.get(exerciseId)
+ );
diff --git a/src/redux/selectors/groups.js b/src/redux/selectors/groups.js
index 369e5007e..3226f8b61 100644
--- a/src/redux/selectors/groups.js
+++ b/src/redux/selectors/groups.js
@@ -12,27 +12,27 @@ import { isReady, getId, getJsData } from '../helpers/resourceManager';
* Select groups part of the state
*/
-export const groupsSelectors = state => state.groups.get('resources');
+export const groupsSelector = state => state.groups.get('resources');
const filterGroups = (ids, groups) =>
groups.filter(isReady).filter(group => ids.contains(getId(group)));
export const groupSelector = id =>
- createSelector(groupsSelectors, groups => groups.get(id));
+ createSelector(groupsSelector, groups => groups.get(id));
export const studentOfSelector = userId =>
createSelector(
- [studentOfGroupsIdsSelector(userId), groupsSelectors],
+ [studentOfGroupsIdsSelector(userId), groupsSelector],
filterGroups
);
export const supervisorOfSelector = userId =>
createSelector(
- [supervisorOfGroupsIdsSelector(userId), groupsSelectors],
+ [supervisorOfGroupsIdsSelector(userId), groupsSelector],
filterGroups
);
export const studentOfSelector2 = userId =>
- createSelector(groupsSelectors, groups =>
+ createSelector(groupsSelector, groups =>
groups
.filter(isReady)
.map(getJsData)
@@ -40,7 +40,7 @@ export const studentOfSelector2 = userId =>
);
export const supervisorOfSelector2 = userId =>
- createSelector(groupsSelectors, groups =>
+ createSelector(groupsSelector, groups =>
groups
.filter(isReady)
.map(getJsData)
@@ -48,7 +48,7 @@ export const supervisorOfSelector2 = userId =>
);
export const adminOfSelector = userId =>
- createSelector(groupsSelectors, groups =>
+ createSelector(groupsSelector, groups =>
groups
.filter(isReady)
.map(getJsData)
@@ -97,4 +97,4 @@ const getGroupParentIds = (id, groups) => {
};
export const allParentIdsForGroup = id =>
- createSelector(groupsSelectors, groups => getGroupParentIds(id, groups));
+ createSelector(groupsSelector, groups => getGroupParentIds(id, groups));
diff --git a/src/redux/store.js b/src/redux/store.js
index 602572154..21a9e5cb8 100644
--- a/src/redux/store.js
+++ b/src/redux/store.js
@@ -7,6 +7,7 @@ import promiseMiddleware from 'redux-promise-middleware';
import * as storage from 'redux-storage';
import authMiddleware from './middleware/authMiddleware';
import apiMiddleware from './middleware/apiMiddleware';
+import loggerMiddleware from './middleware/loggerMiddleware';
import createReducer from './reducer';
import createEngine from 'redux-storage-engine-localstorage';
import filter from 'redux-storage-decorator-filter';
@@ -35,7 +36,10 @@ const getMiddleware = history => [
const dev = history =>
compose(
applyMiddleware(...getMiddleware(history)),
- canUseDOM && window.devToolsExtension ? window.devToolsExtension() : f => f // use the DEVtools if the extension is installed
+ canUseDOM && window.devToolsExtension ? window.devToolsExtension() : f => f, // use the DEVtools if the extension is installed
+ !canUseDOM || !window.devToolsExtension // dev tools not available, or we are at server -> manual logging
+ ? applyMiddleware(loggerMiddleware)
+ : f => f
);
const prod = history => compose(applyMiddleware(...getMiddleware(history)));
diff --git a/src/server.js b/src/server.js
index 9a1cf5019..521d6c2af 100644
--- a/src/server.js
+++ b/src/server.js
@@ -59,6 +59,7 @@ const renderWithoutSSR = (res, renderProps) => {
};
const renderPage = (res, store, renderProps) => {
+ let reduxState = serialize(store.getState(), { isJSON: true });
let html = renderToString(
@@ -69,7 +70,7 @@ const renderPage = (res, store, renderProps) => {
res.render('index', {
html,
head,
- reduxState: serialize(store.getState(), { isJSON: true }),
+ reduxState,
bundle,
style: '/style.css'
});
@@ -87,7 +88,7 @@ app.get('*', (req, res) => {
{ history, routes: createRoutes(store.getState), location },
(error, redirectLocation, renderProps) => {
if (redirectLocation) {
- res.redirect(301, redirectLocation.pathname + redirectLocation.search);
+ res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (error) {
// @todo use the 500.ejs view
res.status(500).send(error.message);
@@ -97,20 +98,18 @@ app.get('*', (req, res) => {
} else {
const userId = loggedInUserIdSelector(store.getState()); // try to get the user ID from the token (if any)
const loadAsync = renderProps.components
- .filter(
- component =>
- component &&
- component.WrappedComponent &&
- component.WrappedComponent.loadAsync
- )
+ .filter(component => component)
.map(component => {
// there might be several layers of wrapping - connect, withLinks, ...
while (component.WrappedComponent) {
component = component.WrappedComponent;
}
- return component.loadAsync;
+ return component;
})
- .map(load => load(renderProps.params, store.dispatch, userId));
+ .filter(component => component.loadAsync)
+ .map(component =>
+ component.loadAsync(renderProps.params, store.dispatch, userId)
+ );
Promise.all(loadAsync)
.then(() => renderPage(res, store, renderProps))