From fd15c0456bbc087ea2287622619db6694af12356 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Fri, 24 Nov 2017 12:03:02 +0100 Subject: [PATCH 01/47] Make Group page a bit faster --- src/pages/Group/Group.js | 70 ++++++++++---------------------- src/pages/User/User.js | 5 ++- src/redux/selectors/exercises.js | 18 ++++++++ src/redux/selectors/groups.js | 23 ++++++++--- src/redux/selectors/instances.js | 2 + src/redux/selectors/stats.js | 18 ++++++++ src/redux/selectors/users.js | 32 ++++++++++++++- 7 files changed, 110 insertions(+), 58 deletions(-) diff --git a/src/pages/Group/Group.js b/src/pages/Group/Group.js index cb4baece4..c9093fca6 100644 --- a/src/pages/Group/Group.js +++ b/src/pages/Group/Group.js @@ -21,7 +21,6 @@ import HierarchyLine from '../../components/Groups/HierarchyLine'; import { EditIcon } from '../../components/icons'; import { LocalizedGroupName } from '../../components/helpers/LocalizedNames'; -import { isReady, getJsData } from '../../redux/helpers/resourceManager'; import { createGroup, fetchGroupIfNeeded, @@ -41,26 +40,24 @@ import { import { fetchInstancePublicGroups } from '../../redux/modules/publicGroups'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { - readyUsersDataSelector, isStudentOf, isSupervisorOf, isAdminOf, - isLoggedAsSuperAdmin + isLoggedAsSuperAdmin, + supervisorsOfGroupSelector, + studentsOfGroupSelector } from '../../redux/selectors/users'; import { groupSelector, groupsSelector, - groupsAssignmentsSelector, - supervisorsOfGroup, - studentsOfGroup + groupsPublicAssignmentsSelector, + groupsAllAssignmentsSelector } from '../../redux/selectors/groups'; -import { getExercisesByIdsSelector } from '../../redux/selectors/exercises'; +import { getExercisesForGroup } from '../../redux/selectors/exercises'; import { publicGroupsSelectors } from '../../redux/selectors/publicGroups'; -import { getStatuses } from '../../redux/selectors/stats'; -import { fetchInstanceIfNeeded } from '../../redux/modules/instances'; -import { instanceSelector } from '../../redux/selectors/instances'; +import { getStatusesForLoggedUser } from '../../redux/selectors/stats'; import { getLocalizedName } from '../../helpers/getLocalizedData'; import withLinks from '../../hoc/withLinks'; @@ -77,7 +74,6 @@ class Group extends Component { Promise.all([ dispatch(fetchGroupIfNeeded(groupId)).then(res => res.value).then(group => Promise.all([ - dispatch(fetchInstanceIfNeeded(group.instanceId)), dispatch(fetchSupervisors(groupId)), dispatch(fetchInstancePublicGroups(group.instanceId)), Group.isAdminOrSupervisorOf(group, userId) || isSuperAdmin @@ -104,36 +100,23 @@ class Group extends Component { } componentWillReceiveProps(newProps) { - const { - params: { groupId }, - isAdmin, - isSupervisor, - isStudent, - isSuperAdmin - } = this.props; + const { params: { groupId } } = this.props; - if ( - groupId !== newProps.params.groupId || - (!(isStudent || isSupervisor || isAdmin || isSuperAdmin) && - (newProps.isStudent || - newProps.isSupervisor || - newProps.isAdmin || - newProps.isSuperAdmin)) - ) { + if (groupId !== newProps.params.groupId) { newProps.loadAsync(newProps.userId, newProps.isSuperAdmin); } } getBreadcrumbs = () => { - const { group, instance, intl: { locale } } = this.props; + const { group, intl: { locale } } = this.props; const breadcrumbs = [ { - resource: instance, + resource: group, iconName: 'university', breadcrumb: data => ({ - link: ({ INSTANCE_URI_FACTORY }) => INSTANCE_URI_FACTORY(data.id), - text: data.name, - resource: instance + link: ({ INSTANCE_URI_FACTORY }) => + INSTANCE_URI_FACTORY(data.instanceId), + text: 'Instance' }) }, { @@ -304,30 +287,19 @@ Group.contextTypes = { }; const mapStateToProps = (state, { params: { groupId } }) => { - const group = groupSelector(groupId)(state); - const groupData = getJsData(group); const userId = loggedInUserIdSelector(state); - const supervisorsIds = supervisorsOfGroup(groupId)(state); - const studentsIds = studentsOfGroup(groupId)(state); - const readyUsers = readyUsersDataSelector(state); - const groupExercisesIds = state.groupExercises[groupId] - ? state.groupExercises[groupId] - : []; return { - group, + group: groupSelector(groupId)(state), userId, - instance: isReady(group) - ? instanceSelector(state, groupData.instanceId) - : null, groups: groupsSelector(state), publicGroups: publicGroupsSelectors(state), - publicAssignments: groupsAssignmentsSelector(groupId, 'public')(state), - allAssignments: groupsAssignmentsSelector(groupId, 'all')(state), - groupExercises: getExercisesByIdsSelector(groupExercisesIds)(state), - statuses: getStatuses(groupId, userId)(state), - supervisors: readyUsers.filter(user => supervisorsIds.includes(user.id)), - students: readyUsers.filter(user => studentsIds.includes(user.id)), + publicAssignments: groupsPublicAssignmentsSelector(state, groupId), + allAssignments: groupsAllAssignmentsSelector(state, groupId), + groupExercises: getExercisesForGroup(state, groupId), + statuses: getStatusesForLoggedUser(state, groupId), + supervisors: supervisorsOfGroupSelector(state, groupId), + students: studentsOfGroupSelector(state, groupId), isStudent: isStudentOf(userId, groupId)(state), isSupervisor: isSupervisorOf(userId, groupId)(state), isAdmin: isAdminOf(userId, groupId)(state), diff --git a/src/pages/User/User.js b/src/pages/User/User.js index 5c62d5e7b..9017d33f4 100644 --- a/src/pages/User/User.js +++ b/src/pages/User/User.js @@ -32,7 +32,7 @@ import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { fetchGroupsStatsIfNeeded } from '../../redux/modules/stats'; import { createGroupsStatsSelector } from '../../redux/selectors/stats'; import { - groupsAssignmentsSelector, + groupsPublicAssignmentsSelector, studentOfSelector2, supervisorOfSelector2, adminOfSelector @@ -312,7 +312,8 @@ export default withLinks( user: getProfile(userId)(state), isAdmin: isSuperadmin, studentOfGroupsIds: studentOfGroupsIdsSelector(userId)(state).toArray(), - groupAssignments: groupId => groupsAssignmentsSelector(groupId)(state), + groupAssignments: groupId => + groupsPublicAssignmentsSelector(state, groupId), groupStatistics: groupId => createGroupsStatsSelector(groupId)(state), usersStatistics: statistics => statistics.find(stat => stat.userId === userId) || {}, diff --git a/src/redux/selectors/exercises.js b/src/redux/selectors/exercises.js index fb09486af..4951f99a0 100644 --- a/src/redux/selectors/exercises.js +++ b/src/redux/selectors/exercises.js @@ -2,8 +2,12 @@ import { createSelector } from 'reselect'; import { isReady } from '../helpers/resourceManager'; import { fetchManyEndpoint } from '../modules/exercises'; +const getParam = (state, id) => id; +const EMPTY_ARR = []; + const getExercises = state => state.exercises; const getResources = exercises => exercises.get('resources'); +const getGroupExercises = state => state.groupExercises; export const exercisesSelector = createSelector(getExercises, getResources); export const exerciseSelector = exerciseId => @@ -27,3 +31,17 @@ export const getExercisesByIdsSelector = ids => .filter(isReady) .filter(exercise => ids.indexOf(exercise.getIn(['data', 'id'])) >= 0) ); + +export const getExercisesForGroup = createSelector( + [exercisesSelector, getGroupExercises, getParam], + (exercises, groupExercises, groupId) => { + const groupExIds = groupExercises[groupId] + ? groupExercises[groupId] + : EMPTY_ARR; + return exercises + .filter(isReady) + .filter( + exercise => groupExIds.indexOf(exercise.getIn(['data', 'id'])) >= 0 + ); + } +); diff --git a/src/redux/selectors/groups.js b/src/redux/selectors/groups.js index 94b3ec04d..650f23418 100644 --- a/src/redux/selectors/groups.js +++ b/src/redux/selectors/groups.js @@ -11,7 +11,9 @@ import { isReady, getId, getJsData } from '../helpers/resourceManager'; /** * Select groups part of the state */ +const getParam = (state, id) => id; const EMPTY_MAP = Map(); +const EMPTY_LIST = List(); export const groupsSelector = state => state.groups.get('resources'); @@ -83,12 +85,21 @@ export const groupsAssignmentsIdsSelector = (id, type = 'public') => : List() ); -export const groupsAssignmentsSelector = (id, type = 'public') => - createSelector( - [groupsAssignmentsIdsSelector(id, type), getAssignments], - (groupsAssignmentsIds, assignments) => - groupsAssignmentsIds.map(id => assignments.getIn(['resources', id])) - ); +export const groupsPublicAssignmentsSelector = createSelector( + [groupsSelector, getAssignments, getParam], + (groups, assignments, groupId) => + groups + .getIn([groupId, 'data', 'assignments', 'public'], EMPTY_LIST) + .map(id => assignments.getIn(['resources', id])) +); + +export const groupsAllAssignmentsSelector = createSelector( + [groupsSelector, getAssignments, getParam], + (groups, assignments, groupId) => + groups + .getIn([groupId, 'data', 'assignments', 'all'], EMPTY_LIST) + .map(id => assignments.getIn(['resources', id])) +); const getGroupParentIds = (id, groups) => { const group = groups.get(id); diff --git a/src/redux/selectors/instances.js b/src/redux/selectors/instances.js index 7e5175f8c..45a752ef7 100644 --- a/src/redux/selectors/instances.js +++ b/src/redux/selectors/instances.js @@ -11,6 +11,8 @@ export const instanceSelector = createSelector( (instances, id) => instances.get(id) ); +// export const instanceForGroupSelector = + export const publicInstancesSelector = createSelector( instancesSelector, instances => diff --git a/src/redux/selectors/stats.js b/src/redux/selectors/stats.js index 49b277cae..9902f6f81 100644 --- a/src/redux/selectors/stats.js +++ b/src/redux/selectors/stats.js @@ -1,10 +1,16 @@ import { createSelector } from 'reselect'; +import { List } from 'immutable'; import { getJsData } from '../helpers/resourceManager'; +import { loggedInUserIdSelector } from './auth'; /** * Public selectors */ +const getParam = (state, id) => id; +const EMPTY_LIST = List(); +const EMPTY_OBJ = {}; + export const statisticsSelector = state => state.stats.get('resources'); export const createGroupsStatsSelector = groupId => @@ -25,3 +31,15 @@ export const getStatuses = (groupId, userId) => getUsersStatistics(groupId, userId), stats => (stats ? stats.statuses : {}) ); + +export const getStatusesForLoggedUser = createSelector( + [statisticsSelector, loggedInUserIdSelector, getParam], + (stats, userId, groupId) => { + const data = stats.getIn([groupId, 'data'], EMPTY_LIST); + if (data !== null) { + const statObj = data.toJS().find(stats => stats.userId === userId); + return statObj ? statObj.statuses : EMPTY_OBJ; + } + return EMPTY_OBJ; + } +); diff --git a/src/redux/selectors/users.js b/src/redux/selectors/users.js index 613d83e38..11eb18ba4 100644 --- a/src/redux/selectors/users.js +++ b/src/redux/selectors/users.js @@ -4,11 +4,19 @@ import { fetchManyEndpoint } from '../modules/users'; import { extractLanguageFromUrl } from '../../links'; import { loggedInUserIdSelector } from './auth'; -import { groupSelector, studentsOfGroup, supervisorsOfGroup } from './groups'; +import { + groupSelector, + studentsOfGroup, + supervisorsOfGroup, + groupsSelector +} from './groups'; import { exerciseSelector } from './exercises'; import { pipelineSelector } from './pipelines'; import { isReady, getJsData } from '../helpers/resourceManager'; +const getParam = (state, id) => id; +const EMPTY_LIST = List(); + const getUsers = state => state.users; const getResources = users => users.get('resources'); const getLang = state => @@ -41,6 +49,28 @@ export const readyUsersDataSelector = createSelector( .toArray() ); +export const supervisorsOfGroupSelector = createSelector( + [readyUsersDataSelector, groupsSelector, getParam], + (users, groups, groupId) => + users.filter(user => + groups + .getIn([groupId, 'data', 'supervisors'], EMPTY_LIST) + .toJS() + .includes(user.id) + ) +); + +export const studentsOfGroupSelector = createSelector( + [readyUsersDataSelector, groupsSelector, getParam], + (users, groups, groupId) => + users.filter(user => + groups + .getIn([groupId, 'data', 'students'], EMPTY_LIST) + .toJS() + .includes(user.id) + ) +); + export const isVerified = userId => createSelector( getUser(userId), From 55e8ae720d5a5dabb7110813b1a28ae591aa0ee9 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Fri, 24 Nov 2017 16:03:08 +0100 Subject: [PATCH 02/47] Form for specifying tests --- .../AttachedFilesTable/AttachedFilesTable.js | 2 +- .../Groups/SupervisorsView/SupervisorsView.js | 4 +- .../EditExerciseForm/EditExerciseForm.js | 6 +- .../forms/EditTestsForm/EditTests.css | 4 + .../forms/EditTestsForm/EditTestsForm.js | 114 +++++++++ .../forms/EditTestsForm/EditTestsTest.js | 56 +++++ .../forms/EditTestsForm/EditTestsTestRow.js | 52 +++++ src/components/forms/EditTestsForm/index.js | 1 + src/links/index.js | 3 + .../EditExerciseSimpleConfig.js | 220 ++++++++++++++++++ src/pages/EditExerciseSimpleConfig/index.js | 1 + src/pages/Exercise/Exercise.js | 6 +- src/pages/Exercises/Exercises.js | 7 +- src/pages/routes.js | 5 + 14 files changed, 472 insertions(+), 9 deletions(-) create mode 100644 src/components/forms/EditTestsForm/EditTests.css create mode 100644 src/components/forms/EditTestsForm/EditTestsForm.js create mode 100644 src/components/forms/EditTestsForm/EditTestsTest.js create mode 100644 src/components/forms/EditTestsForm/EditTestsTestRow.js create mode 100644 src/components/forms/EditTestsForm/index.js create mode 100644 src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js create mode 100644 src/pages/EditExerciseSimpleConfig/index.js diff --git a/src/components/Exercises/AttachedFilesTable/AttachedFilesTable.js b/src/components/Exercises/AttachedFilesTable/AttachedFilesTable.js index 9f8a362a1..49e0397ff 100644 --- a/src/components/Exercises/AttachedFilesTable/AttachedFilesTable.js +++ b/src/components/Exercises/AttachedFilesTable/AttachedFilesTable.js @@ -31,7 +31,7 @@ const AttachedFilesTable = ({ RowComponent, intl }) => - +
{description &&

diff --git a/src/components/Groups/SupervisorsView/SupervisorsView.js b/src/components/Groups/SupervisorsView/SupervisorsView.js index 15e333e75..85da5a1d5 100644 --- a/src/components/Groups/SupervisorsView/SupervisorsView.js +++ b/src/components/Groups/SupervisorsView/SupervisorsView.js @@ -30,7 +30,7 @@ const SupervisorsView = ({ deleteExercise, users, publicAssignments, - links: { EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_CONFIG_URI_FACTORY }, + links: { EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY }, intl: { locale } }) =>

@@ -157,7 +157,7 @@ const SupervisorsView = ({
+ ); + } +} + +EditTestsForm.propTypes = { + values: PropTypes.array, + handleSubmit: PropTypes.func.isRequired, + anyTouched: PropTypes.bool, + submitting: PropTypes.bool, + hasFailed: PropTypes.bool, + hasSucceeded: PropTypes.bool, + invalid: PropTypes.bool, + formValues: PropTypes.object +}; + +const validate = () => { + const errors = {}; + + return errors; +}; + +export default connect(state => { + return { + formValues: getFormValues('editTests')(state) + }; +})( + reduxForm({ + form: 'editTests', + validate + })(EditTestsForm) +); diff --git a/src/components/forms/EditTestsForm/EditTestsTest.js b/src/components/forms/EditTestsForm/EditTestsTest.js new file mode 100644 index 000000000..382488306 --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTestsTest.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Table, Button } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; +import { FormattedMessage } from 'react-intl'; +import EditTestsTestRow from './EditTestsTestRow'; + +const EditTestsTest = ({ fields, isUniform }) => +
+ + + + + {!isUniform && + } + + + + {fields.map((test, index) => + fields.remove(index)} + /> + )} + +
+ + + + +
+ +
; + +EditTestsTest.propTypes = { + fields: PropTypes.object.isRequired, + isUniform: PropTypes.bool.isRequired +}; + +export default EditTestsTest; diff --git a/src/components/forms/EditTestsForm/EditTestsTestRow.js b/src/components/forms/EditTestsForm/EditTestsTestRow.js new file mode 100644 index 000000000..0144c39ec --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTestsTestRow.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field } from 'redux-form'; +import { Button } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; +import { FormattedMessage } from 'react-intl'; + +import { TextField } from '../Fields'; +import './EditTests.css'; + +const EditTestsTestRow = ({ test, onRemove, isUniform }) => + + + + + {!isUniform && + + + } + + + + ; + +EditTestsTestRow.propTypes = { + test: PropTypes.string.isRequired, + onRemove: PropTypes.func.isRequired, + isUniform: PropTypes.bool.isRequired +}; + +export default EditTestsTestRow; diff --git a/src/components/forms/EditTestsForm/index.js b/src/components/forms/EditTestsForm/index.js new file mode 100644 index 000000000..8ea2d83dc --- /dev/null +++ b/src/components/forms/EditTestsForm/index.js @@ -0,0 +1 @@ +export default from './EditTestsForm'; diff --git a/src/links/index.js b/src/links/index.js index adcaf0ac0..8f9ab561b 100644 --- a/src/links/index.js +++ b/src/links/index.js @@ -30,6 +30,8 @@ export const linksFactory = lang => { const EXERCISE_EDIT_URI_FACTORY = id => `${EXERCISE_URI_FACTORY(id)}/edit`; const EXERCISE_EDIT_CONFIG_URI_FACTORY = id => `${EXERCISE_URI_FACTORY(id)}/edit-config`; + const EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY = id => + `${EXERCISE_URI_FACTORY(id)}/edit-simple-config`; // reference solution const EXERCISE_REFERENCE_SOLUTION_URI_FACTORY = ( @@ -101,6 +103,7 @@ export const linksFactory = lang => { EXERCISE_URI_FACTORY, EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_CONFIG_URI_FACTORY, + EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, EXERCISE_CREATE_URI_FACTORY, EXERCISE_REFERENCE_SOLUTION_URI_FACTORY, PIPELINES_URI, diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js new file mode 100644 index 000000000..bc25173fd --- /dev/null +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -0,0 +1,220 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { Row, Col } from 'react-bootstrap'; +import { connect } from 'react-redux'; + +import Page from '../../components/layout/Page'; +import Box from '../../components/widgets/Box'; +import ResourceRenderer from '../../components/helpers/ResourceRenderer'; +import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; +import EditSimpleLimitsBox from '../../components/Exercises/EditSimpleLimitsBox'; +import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer'; +import EditTestsForm from '../../components/forms/EditTestsForm'; + +import { fetchExerciseIfNeeded } from '../../redux/modules/exercises'; +import { + fetchExerciseEnvironmentSimpleLimitsIfNeeded, + editEnvironmentSimpleLimits, + setHorizontally, + setVertically, + setAll +} from '../../redux/modules/simpleLimits'; +import { fetchExerciseConfigIfNeeded } from '../../redux/modules/exerciseConfigs'; +import { getExercise } from '../../redux/selectors/exercises'; +import { exerciseConfigSelector } from '../../redux/selectors/exerciseConfigs'; +import { loggedInUserIdSelector } from '../../redux/selectors/auth'; +import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; +import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments'; +import { simpleLimitsSelector } from '../../redux/selectors/simpleLimits'; + +import withLinks from '../../hoc/withLinks'; +import { getLocalizedName } from '../../helpers/getLocalizedData'; + +class EditExerciseSimpleConfig extends Component { + componentWillMount = () => this.props.loadAsync(); + componentWillReceiveProps = props => { + if (this.props.params.exerciseId !== props.params.exerciseId) { + props.loadAsync(); + } + }; + + static loadAsync = ({ exerciseId }, dispatch) => + Promise.all([ + dispatch(fetchExerciseIfNeeded(exerciseId)).then(({ value: exercise }) => + Promise.all( + exercise.runtimeEnvironments.map(environment => + dispatch( + fetchExerciseEnvironmentSimpleLimitsIfNeeded( + exerciseId, + environment.id + ) + ) + ) + ) + ), + dispatch(fetchExerciseConfigIfNeeded(exerciseId)), + dispatch(fetchRuntimeEnvironments()) + ]); + + render() { + const { + links: { EXERCISE_URI_FACTORY }, + params: { exerciseId }, + exercise, + runtimeEnvironments, + exerciseConfig, + editEnvironmentSimpleLimits, + limits, + setHorizontally, + setVertically, + setAll, + intl: { locale } + } = this.props; + + return ( + } + description={ + + } + breadcrumbs={[ + { + resource: exercise, + breadcrumb: ({ name, localizedTexts }) => ({ + text: ( + + ), + iconName: 'puzzle-piece', + link: EXERCISE_URI_FACTORY(exerciseId) + }) + }, + { + text: ( + + ), + iconName: 'pencil' + } + ]} + > + {exercise => +
+ + + + } + unlimitedHeight + > + console.log(data)} + /> + + + + + + + +
+ + + + {(config, ...runtimeEnvironments) => +
+ +
} +
+ +
+
} +
+ ); + } +} + +EditExerciseSimpleConfig.propTypes = { + exercise: ImmutablePropTypes.map, + runtimeEnvironments: PropTypes.object.isRequired, + loadAsync: PropTypes.func.isRequired, + params: PropTypes.shape({ + exerciseId: PropTypes.string.isRequired + }).isRequired, + exerciseConfig: PropTypes.object, + editEnvironmentSimpleLimits: PropTypes.func.isRequired, + links: PropTypes.object.isRequired, + limits: PropTypes.func.isRequired, + setHorizontally: PropTypes.func.isRequired, + setVertically: PropTypes.func.isRequired, + setAll: PropTypes.func.isRequired, + intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired +}; + +export default injectIntl( + withLinks( + connect( + (state, { params: { exerciseId } }) => { + return { + exercise: getExercise(exerciseId)(state), + userId: loggedInUserIdSelector(state), + runtimeEnvironments: runtimeEnvironmentsSelector(state), + exerciseConfig: exerciseConfigSelector(exerciseId)(state), + limits: runtimeEnvironmentId => + simpleLimitsSelector(exerciseId, runtimeEnvironmentId)(state) + }; + }, + (dispatch, { params: { exerciseId } }) => ({ + loadAsync: () => + EditExerciseSimpleConfig.loadAsync({ exerciseId }, dispatch), + editEnvironmentSimpleLimits: runtimeEnvironmentId => data => + dispatch( + editEnvironmentSimpleLimits(exerciseId, runtimeEnvironmentId, data) + ), + setHorizontally: (formName, runtimeEnvironmentId) => testName => () => + dispatch( + setHorizontally( + formName, + exerciseId, + runtimeEnvironmentId, + testName + ) + ), + setVertically: (formName, runtimeEnvironmentId) => testName => () => + dispatch( + setVertically(formName, exerciseId, runtimeEnvironmentId, testName) + ), + setAll: (formName, runtimeEnvironmentId) => testName => () => + dispatch(setAll(formName, exerciseId, runtimeEnvironmentId, testName)) + }) + )(EditExerciseSimpleConfig) + ) +); diff --git a/src/pages/EditExerciseSimpleConfig/index.js b/src/pages/EditExerciseSimpleConfig/index.js new file mode 100644 index 000000000..4e440ba6e --- /dev/null +++ b/src/pages/EditExerciseSimpleConfig/index.js @@ -0,0 +1 @@ +export default from './EditExerciseSimpleConfig'; diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index ebc1b32dd..a4114370e 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -150,7 +150,7 @@ class Exercise extends Component { links: { EXERCISES_URI, EXERCISE_EDIT_URI_FACTORY, - EXERCISE_EDIT_CONFIG_URI_FACTORY, + EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, EXERCISE_REFERENCE_SOLUTION_URI_FACTORY, PIPELINE_EDIT_URI_FACTORY } @@ -208,7 +208,9 @@ class Exercise extends Component {
; SecondsTextField.propTypes = { input: PropTypes.shape({ diff --git a/src/components/forms/Fields/index.js b/src/components/forms/Fields/index.js index 8806625ff..176881806 100644 --- a/src/components/forms/Fields/index.js +++ b/src/components/forms/Fields/index.js @@ -2,6 +2,7 @@ export { default as CheckboxField } from './CheckboxField'; export { default as EmailField } from './EmailField'; export { default as DatetimeField } from './DatetimeField'; export { default as MarkdownTextAreaField } from './MarkdownTextAreaField'; +export { default as EditSimpleLimitsField } from './EditSimpleLimitsField'; export { default as LimitsField } from './LimitsField'; export { default as PasswordField } from './PasswordField'; export { default as PasswordStrength } from './PasswordStrength'; diff --git a/src/locales/cs.json b/src/locales/cs.json index c5e9ed9fe..03ba257be 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -502,8 +502,8 @@ "app.feedbackAndBugs.whereToReportBugs": "Where can I report bugs?", "app.feedbackAndBugs.whereToReportBugsText": "Every software contains bugs and we are well avare of this fact. From time to time you might find a bug that nobody else has reported and which hasn't been fixed yet. Please report all bugs to our issue tracker on GitHub - just file a new issue and give it a label 'bug'. We will try to investigate and release a bugfix as soon as possible.", "app.field.isRequired": "This field is required.", - "app.fields.limits.memory": "Test memory limit:", - "app.fields.limits.time": "Test time limit:", + "app.fields.limits.memory": "Paměťový limit [KiB]:", + "app.fields.limits.time": "Časový limit [s]:", "app.footer.copyright": "Copyright © 2016-2017 ReCodEx. Všechna práva vyhrazena.", "app.footer.version": "Verze {version}", "app.forkExerciseButton.confirmation": "Opravdu chcete zduplokovat tuto úlohu?", diff --git a/src/locales/en.json b/src/locales/en.json index 4ae6aacfe..95fa7ebb7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -502,8 +502,8 @@ "app.feedbackAndBugs.whereToReportBugs": "Where can I report bugs?", "app.feedbackAndBugs.whereToReportBugsText": "Every software contains bugs and we are well avare of this fact. From time to time you might find a bug that nobody else has reported and which hasn't been fixed yet. Please report all bugs to our issue tracker on GitHub - just file a new issue and give it a label 'bug'. We will try to investigate and release a bugfix as soon as possible.", "app.field.isRequired": "This field is required.", - "app.fields.limits.memory": "Test memory limit:", - "app.fields.limits.time": "Test time limit:", + "app.fields.limits.memory": "Memory Limit [KiB]:", + "app.fields.limits.time": "Time Limit [s]:", "app.footer.copyright": "Copyright © 2016-2017 ReCodEx. All rights reserved.", "app.footer.version": "Version {version}", "app.forkExerciseButton.confirmation": "Do you really want to fork this exercise?", diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index 4f248bec4..8232736ea 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -10,7 +10,7 @@ import Page from '../../components/layout/Page'; import Box from '../../components/widgets/Box'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; -import EditSimpleLimitsBox from '../../components/Exercises/EditSimpleLimitsBox'; +import EditSimpleLimitsForm from '../../components/forms/EditSimpleLimits/EditSimpleLimitsForm'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer'; import EditTestsForm from '../../components/forms/EditTestsForm'; import EditExerciseSimpleConfigForm from '../../components/forms/EditExerciseSimpleConfigForm'; @@ -30,7 +30,7 @@ import { exerciseConfigSelector } from '../../redux/selectors/exerciseConfigs'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments'; -import { simpleLimitsSelector } from '../../redux/selectors/simpleLimits'; +import { simpleLimitsAllSelector } from '../../redux/selectors/simpleLimits'; import withLinks from '../../hoc/withLinks'; import { getLocalizedName } from '../../helpers/getLocalizedData'; @@ -206,6 +206,27 @@ const getSimpleConfigInitValues = (config, tests, locale) => { return { config: res }; }; +const getLimitsInitValues = (limits, tests, environments) => { + let res = {}; + + tests.forEach(test => { + const testId = 'test' + btoa(test.name); // the name can be anything, but it must be compatible with redux-form + res[testId] = {}; + environments.forEach(environment => { + const envId = 'env' + btoa(environment.id); // the name can be anything, but it must be compatible with redux-form + res[testId][envId] = + limits[environment.id] && limits[environment.id][test.name] + ? limits[environment.id][test.name] + : { + memory: 0, + 'wall-time': 0 + }; + }); + }); + + return { limits: res }; +}; + class EditExerciseSimpleConfig extends Component { componentWillMount = () => this.props.loadAsync(); componentWillReceiveProps = props => { @@ -398,7 +419,28 @@ class EditExerciseSimpleConfig extends Component { - + + {( + tests // todo add limits here, so the form wait for them to load and update getLimitsInitValues + ) => + + a.name.localeCompare(b.name, locale) + )} + initialValues={getLimitsInitValues( + limits, + tests, + exercise.runtimeEnvironments + )} + setVertically={setVertically} + setHorizontally={setHorizontally} + setAll={setAll} + />} + + {/* + {config =>
-
} -
+ +
+ */}
} @@ -436,7 +479,7 @@ EditExerciseSimpleConfig.propTypes = { editScoreConfig: PropTypes.func.isRequired, editTests: PropTypes.func.isRequired, links: PropTypes.object.isRequired, - limits: PropTypes.func.isRequired, + limits: PropTypes.object.isRequired, setHorizontally: PropTypes.func.isRequired, setVertically: PropTypes.func.isRequired, setAll: PropTypes.func.isRequired, @@ -452,8 +495,7 @@ export default injectIntl( userId: loggedInUserIdSelector(state), runtimeEnvironments: runtimeEnvironmentsSelector(state), exerciseConfig: exerciseConfigSelector(exerciseId)(state), - limits: runtimeEnvironmentId => - simpleLimitsSelector(exerciseId, runtimeEnvironmentId)(state), + limits: simpleLimitsAllSelector(exerciseId)(state), exerciseEnvironmentConfig: exerciseEnvironmentConfigSelector( exerciseId )(state), diff --git a/src/redux/middleware/loggerMiddleware.js b/src/redux/middleware/loggerMiddleware.js index 368b019dc..881d08fc8 100644 --- a/src/redux/middleware/loggerMiddleware.js +++ b/src/redux/middleware/loggerMiddleware.js @@ -1,6 +1,6 @@ const middleware = isDev => store => next => action => { /* eslint no-console: ["error", { allow: ["log", "error"] }] */ - let verbose = false; + let verbose = true; var actionType = action.type; if (verbose) { console.log('Starting ' + actionType); diff --git a/src/redux/selectors/exercises.js b/src/redux/selectors/exercises.js index 4951f99a0..024291955 100644 --- a/src/redux/selectors/exercises.js +++ b/src/redux/selectors/exercises.js @@ -20,6 +20,12 @@ export const fetchManyStatus = createSelector(getExercises, state => export const getExercise = id => createSelector(getExercises, exercises => exercises.getIn(['resources', id])); +export const getExerciseRuntimeEnvironments = id => + createSelector( + getExercise(id), + exercise => exercise && exercise.getIn(['data', 'runtimeEnvironments']) + ); + export const getFork = (id, forkId) => createSelector(getExercise(id), exercise => exercise.getIn(['data', 'forks', forkId]) diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js index dfa7f1966..c2944af56 100644 --- a/src/redux/selectors/simpleLimits.js +++ b/src/redux/selectors/simpleLimits.js @@ -1,8 +1,11 @@ import { createSelector } from 'reselect'; - +import { Map } from 'immutable'; +import { getExerciseRuntimeEnvironments } from './exercises'; import { endpointDisguisedAsIdFactory } from '../modules/simpleLimits'; const getLimits = state => state.simpleLimits; +const EMPTY_OBJ = {}; +const EMPTY_MAP = Map(); export const simpleLimitsSelector = (exerciseId, runtimeEnvironmentId) => createSelector(getLimits, limits => @@ -14,3 +17,30 @@ export const simpleLimitsSelector = (exerciseId, runtimeEnvironmentId) => }) ]) ); + +export const simpleLimitsAllSelector = exerciseId => + createSelector( + [getLimits, getExerciseRuntimeEnvironments(exerciseId)], + (limits, runtimeEnvironments) => { + if (!limits || !runtimeEnvironments) { + return EMPTY_OBJ; + } + + return runtimeEnvironments.reduce((acc, runtimeEnvironment) => { + const runtimeEnvironmentId = runtimeEnvironment.get('id'); + let testLimits = limits.getIn([ + 'resources', + endpointDisguisedAsIdFactory({ + exerciseId, + runtimeEnvironmentId + }), + 'data' + ]); + if (testLimits) { + testLimits = testLimits.toJS(); + } + acc[runtimeEnvironmentId] = testLimits; + return acc; + }, {}); + } + ); diff --git a/yarn.lock b/yarn.lock index 23bfcdb54..263ec3d08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3482,6 +3482,10 @@ hoist-non-react-statics@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.2.2.tgz#c0eca5a7d5a28c5ada3107eb763b01da6bfa81fb" +hoist-non-react-statics@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -6365,13 +6369,13 @@ redux-devtools-instrument@^1.3.3: lodash "^4.2.0" symbol-observable "^1.0.2" -redux-form@^6.2.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-6.8.0.tgz#ff1b590b59f987d7e3ff080d752f7120bfe42af3" +redux-form@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.2.0.tgz#4465d9bc863e40b1704695d672bea75fcf81db04" dependencies: deep-equal "^1.0.1" es6-error "^4.0.0" - hoist-non-react-statics "^1.2.0" + hoist-non-react-statics "^2.3.1" invariant "^2.2.2" is-promise "^2.1.0" lodash "^4.17.3" From 2d18ef2000a56e7be72098093d890caf16fb8cf0 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Tue, 5 Dec 2017 18:48:24 +0100 Subject: [PATCH 20/47] Simple limits edit form rewritten once more + now supports field validation and cloning buttons. --- .../EditSimpleLimits/EditSimpleLimitsForm.js | 42 ++-- .../forms/Fields/EditSimpleLimitsField.js | 182 ++++++++++++------ .../forms/Fields/EditSimpleLimitsField.less | 2 +- src/helpers/exerciseSimpleForm.js | 39 ++++ .../EditExerciseSimpleConfig.js | 65 ++++--- src/redux/modules/simpleLimits.js | 145 +++++++++----- src/redux/selectors/simpleLimits.js | 6 + 7 files changed, 330 insertions(+), 151 deletions(-) diff --git a/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js index f3b998f3b..db2e15d34 100644 --- a/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js @@ -8,15 +8,20 @@ import { EditSimpleLimitsField } from '../Fields'; import SubmitButton from '../SubmitButton'; import FormBox from '../../widgets/FormBox'; +import { + encodeTestName, + encodeEnvironmentId +} from '../../../redux/modules/simpleLimits'; + import styles from './styles.less'; const EditSimpleLimitsForm = ({ environments, editLimits, tests, - setHorizontally, - setVertically, - setAll, + cloneHorizontally, + cloneVertically, + cloneAll, anyTouched, submitting, submitFailed, @@ -104,7 +109,9 @@ const EditSimpleLimitsForm = ({ {environments.map(environment => { const id = - 'test' + btoa(test.name) + '.env' + btoa(environment.id); + encodeTestName(test.name) + + '.' + + encodeEnvironmentId(environment.id); return ( ); @@ -141,9 +160,9 @@ EditSimpleLimitsForm.propTypes = { tests: PropTypes.array.isRequired, environments: PropTypes.array, editLimits: PropTypes.func.isRequired, - setHorizontally: PropTypes.func.isRequired, - setVertically: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired, + cloneHorizontally: PropTypes.func.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, anyTouched: PropTypes.bool, submitting: PropTypes.bool, submitFailed: PropTypes.bool, @@ -226,6 +245,5 @@ const validate = ({ limits }) => { export default reduxForm({ form: 'editSimpleLimits', enableReinitialize: true, - keepDirtyOnReinitialize: true, - validate + keepDirtyOnReinitialize: true })(EditSimpleLimitsForm); diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js index bcaa64f7a..240275f4e 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Field, formValues } from 'redux-form'; +import { Field } from 'redux-form'; import { FormattedMessage } from 'react-intl'; import { Row, Col } from 'react-bootstrap'; import Icon from 'react-fontawesome'; @@ -14,16 +14,90 @@ import prettyMs from 'pretty-ms'; import styles from './EditSimpleLimitsField.less'; +const LimitsValueField = ({ input, prettyPrint, ...props }) => + + + + + + + {prettyPrint(input.value)} + + + ; + +LimitsValueField.propTypes = { + input: PropTypes.shape({ + value: PropTypes.any.isRequired + }).isRequired, + prettyPrint: PropTypes.func.isRequired +}; + +const prettyPrintBytesWrap = value => + Number.isNaN(Number(value)) ? '-' : prettyPrintBytes(Number(value) * 1024); + +const prettyPrintMsWrap = value => + Number.isNaN(Number(value)) ? '-' : prettyMs(Number(value) * 1000); + +/** + * These limits are only soft limits applied in webapp. + * Note that hard maxima are in worker configuration /etc/recodex/worker on all worker hosts. + * If you need to change this, worker limits should probably be changed as well. + */ +const limitRanges = { + memory: { + min: 128, + max: 1024 * 1024 // 1GiB + }, + time: { + min: 0.1, + max: 60 + } +}; + +const validateValue = (ranges, pretty) => value => { + const num = Number(value); + if (Number.isNaN(num)) { + return ( + + ); + } + + if (num < ranges.min) { + return ( + + ); + } + if (num > ranges.max) { + return ( + + ); + } + return undefined; +}; + +const validateMemory = validateValue(limitRanges.memory, prettyPrintBytes); +const validateTime = validateValue(limitRanges.time, prettyMs); + const EditSimpleLimitsField = ({ prefix, id, - memoryValue, - wallTimeValue, testsCount, environmentsCount, - setHorizontally, - setVertically, - setAll, + cloneVertically, + cloneHorizontally, + cloneAll, ...props }) =>
@@ -31,36 +105,29 @@ const EditSimpleLimitsField = ({ = 3 ? 12 : 6} md={12}> - - + } + validate={validateMemory} + {...props} + /> + -
- - } - {...props} + -
- - {Number.isNaN(Number(memoryValue)) - ? '-' - : prettyPrintBytes(Number(memoryValue) * 1024)} - -
- {testsCount > 1 && - + } {environmentsCount > 1 && 1 && = 3 ? 12 : 6} md={12}> - - + } + {...props} + /> + - {( - tests // todo add limits here, so the form wait for them to load and update getLimitsInitValues - ) => + {tests => } {/* @@ -355,9 +355,9 @@ EditExerciseSimpleConfig.propTypes = { links: PropTypes.object.isRequired, limits: PropTypes.object.isRequired, pipelines: ImmutablePropTypes.map, - setHorizontally: PropTypes.func.isRequired, - setVertically: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired, + cloneHorizontally: PropTypes.func.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; @@ -370,7 +370,7 @@ export default injectIntl( userId: loggedInUserIdSelector(state), runtimeEnvironments: runtimeEnvironmentsSelector(state), exerciseConfig: exerciseConfigSelector(exerciseId)(state), - limits: simpleLimitsAllSelector(exerciseId)(state), + limits: simpleLimitsSelector(state), exerciseEnvironmentConfig: exerciseEnvironmentConfigSelector( exerciseId )(state), @@ -391,21 +391,24 @@ export default injectIntl( editScoreConfig: data => dispatch(setScoreConfig(exerciseId, data)), editTests: data => dispatch(setExerciseTests(exerciseId, data)), setConfig: data => dispatch(setExerciseConfig(exerciseId, data)), - setHorizontally: (formName, runtimeEnvironmentId) => testName => () => + cloneVertically: ( + formName, + testName, + runtimeEnvironmentId + ) => field => () => dispatch( - setHorizontally( - formName, - exerciseId, - runtimeEnvironmentId, - testName - ) + cloneVertically(formName, testName, runtimeEnvironmentId, field) ), - setVertically: (formName, runtimeEnvironmentId) => testName => () => + cloneHorizontally: ( + formName, + testName, + runtimeEnvironmentId + ) => field => () => dispatch( - setVertically(formName, exerciseId, runtimeEnvironmentId, testName) + cloneHorizontally(formName, testName, runtimeEnvironmentId, field) ), - setAll: (formName, runtimeEnvironmentId) => testName => () => - dispatch(setAll(formName, exerciseId, runtimeEnvironmentId, testName)) + cloneAll: (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)) }) )(EditExerciseSimpleConfig) ) diff --git a/src/redux/modules/simpleLimits.js b/src/redux/modules/simpleLimits.js index 7e88ddea8..460668066 100644 --- a/src/redux/modules/simpleLimits.js +++ b/src/redux/modules/simpleLimits.js @@ -49,17 +49,7 @@ export const editEnvironmentSimpleLimits = ( data ); -const getSimpleLimitsOf = ( - { form }, - formName, - exerciseId, - runtimeEnvironmentId, - testName -) => - form[formName].values.limits[testName] || - form[formName].values.initial[testName] || - {}; - +/* const isSimpleLimitsForm = ({ registeredFields }, testName) => registeredFields && registeredFields.hasOwnProperty(`limits.${testName}.wall-time`) && @@ -78,64 +68,131 @@ const getAllTestNames = ({ form }, formName) => .reduce((acc, name) => (acc.indexOf(name) >= 0 ? acc : [...acc, name]), []); const field = testName => `limits.${testName}`; +*/ -export const setVertically = ( +/* + * Special functions for cloning buttons + */ + +// Encoding function which help us avoid problems with some characters in test names and env ids (e.g., character '.'). +export const encodeTestName = testName => 'test' + btoa(testName); +export const encodeEnvironmentId = envId => 'env' + btoa(envId); + +// Get a single value by its test name, environment ID, and field identifier +const getSimpleLimitsOf = ( + { form }, formName, - exerciseId, + testName, runtimeEnvironmentId, - testName + field +) => { + const testEnc = encodeTestName(testName); + const envEnc = encodeEnvironmentId(runtimeEnvironmentId); + return ( + form[formName].values.limits[testEnc][envEnc][field] || + form[formName].initial.limits[testName][envEnc][field] || + null + ); +}; + +// Lists all form keys to which the value should be copied +const getTargetFormKeys = ( + { form }, // form key in the store + formName, // form identifier + testName, // test name or null (if all test should be targetted) + environmentId, // environment ID or null (if all environments should be targetted) + field // field identifier (memory or wall-time) +) => { + const testEnc = testName ? encodeTestName(testName) : null; + const envEnc = environmentId ? encodeEnvironmentId(environmentId) : null; + return form && form[formName] && form[formName].registeredFields + ? Object.keys(form[formName].registeredFields).filter(key => { + const [, test, env, f] = key.split('.'); + return ( + (!testEnc || test === testEnc) && + (!envEnc || env === envEnc) && + f === field + ); + }) + : []; +}; + +// Clone given value vertically (all test in environment) +export const cloneVertically = ( + formName, // form identifier + testName, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or wall-time) ) => (dispatch, getState) => { const state = getState(); - const data = getSimpleLimitsOf( + const value = getSimpleLimitsOf( state, formName, - exerciseId, + testName, runtimeEnvironmentId, - testName - ); - getAllTestNames(getState(), formName).map(testName => - dispatch(change(formName, field(testName), data)) + field ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + null, // no test name => all test selected + runtimeEnvironmentId, + field + ).map(key => dispatch(change(formName, key, value))); + } }; -export const setHorizontally = ( - formName, - exerciseId, - runtimeEnvironmentId, - testName +// Clone given value horizontally (all environments of the same test) +export const cloneHorizontally = ( + formName, // form identifier + testName, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or wall-time) ) => (dispatch, getState) => { const state = getState(); - const data = getSimpleLimitsOf( + const value = getSimpleLimitsOf( state, formName, - exerciseId, + testName, runtimeEnvironmentId, - testName - ); - getAllSimpleLimitsFormNames(getState(), testName).map(formName => - dispatch(change(formName, field(testName), data)) + field ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + testName, + null, // no environemnt ID => all environments accepted + field + ).map(key => dispatch(change(formName, key, value))); + } }; -export const setAll = ( - formName, - exerciseId, - runtimeEnvironmentId, - testName +// Clone given value to all fields +export const cloneAll = ( + formName, // form identifier + testName, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or wall-time) ) => (dispatch, getState) => { const state = getState(); - const data = getSimpleLimitsOf( + const value = getSimpleLimitsOf( state, formName, - exerciseId, + testName, runtimeEnvironmentId, - testName - ); - getAllSimpleLimitsFormNames(getState(), testName).map(formName => - getAllTestNames(getState(), formName).map(testName => - dispatch(change(formName, field(testName), data)) - ) + field ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + null, // no test name ... + null, // ... nor environemnt ID => all fields + field + ).map(key => dispatch(change(formName, key, value))); + } }; const reducer = handleActions(reduceActions, initialState); diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js index c2944af56..143776990 100644 --- a/src/redux/selectors/simpleLimits.js +++ b/src/redux/selectors/simpleLimits.js @@ -7,6 +7,7 @@ const getLimits = state => state.simpleLimits; const EMPTY_OBJ = {}; const EMPTY_MAP = Map(); +/* export const simpleLimitsSelector = (exerciseId, runtimeEnvironmentId) => createSelector(getLimits, limits => limits.getIn([ @@ -17,6 +18,11 @@ export const simpleLimitsSelector = (exerciseId, runtimeEnvironmentId) => }) ]) ); +*/ + +export const simpleLimitsSelector = createSelector(getLimits, limits => + limits.get('resources') +); export const simpleLimitsAllSelector = exerciseId => createSelector( From e3efcb7b0c6a47c4295932ad0e321d09026cd37c Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 7 Dec 2017 01:06:44 +0100 Subject: [PATCH 21/47] Finishing work on simple limits form. --- .../EditSimpleLimitsBox.js | 26 --- .../Exercises/EditSimpleLimitsBox/index.js | 1 - .../EditEnvironmentLimitsFields.js | 70 ------- .../EditEnvironmentSimpleLimitsForm.js | 177 ------------------ .../EditSimpleLimits/EditSimpleLimits.js | 81 -------- .../EditSimpleLimitsForm.js | 143 ++++++-------- .../index.js | 0 .../styles.less | 0 .../forms/Fields/EditSimpleLimitsField.js | 25 +-- .../forms/Fields/KiloBytesTextField.js | 59 +----- src/components/forms/Fields/LimitsField.js | 1 + .../forms/Fields/LimitsValueField.js | 27 +++ .../forms/Fields/SecondsTextField.js | 1 + src/components/forms/Fields/TextField.js | 12 +- src/components/forms/Fields/index.js | 1 + src/helpers/exerciseSimpleForm.js | 47 ++++- src/locales/cs.json | 66 ++++++- src/locales/en.json | 66 ++++++- .../EditExerciseConfig/EditExerciseConfig.js | 32 +--- .../EditExerciseSimpleConfig.js | 56 ++---- src/redux/middleware/loggerMiddleware.js | 27 +-- src/redux/modules/simpleLimits.js | 21 --- src/redux/selectors/simpleLimits.js | 15 -- 23 files changed, 300 insertions(+), 654 deletions(-) delete mode 100644 src/components/Exercises/EditSimpleLimitsBox/EditSimpleLimitsBox.js delete mode 100644 src/components/Exercises/EditSimpleLimitsBox/index.js delete mode 100644 src/components/forms/EditSimpleLimits/EditEnvironmentLimitsFields.js delete mode 100644 src/components/forms/EditSimpleLimits/EditEnvironmentSimpleLimitsForm.js delete mode 100644 src/components/forms/EditSimpleLimits/EditSimpleLimits.js rename src/components/forms/{EditSimpleLimits => EditSimpleLimitsForm}/EditSimpleLimitsForm.js (60%) rename src/components/forms/{EditSimpleLimits => EditSimpleLimitsForm}/index.js (100%) rename src/components/forms/{EditSimpleLimits => EditSimpleLimitsForm}/styles.less (100%) create mode 100644 src/components/forms/Fields/LimitsValueField.js diff --git a/src/components/Exercises/EditSimpleLimitsBox/EditSimpleLimitsBox.js b/src/components/Exercises/EditSimpleLimitsBox/EditSimpleLimitsBox.js deleted file mode 100644 index b8fd9a347..000000000 --- a/src/components/Exercises/EditSimpleLimitsBox/EditSimpleLimitsBox.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -import Box from '../../widgets/Box'; -import EditSimpleLimits from '../../forms/EditSimpleLimits'; - -const EditSimpleLimitsBox = ({ editLimits, limits, ...props }) => - - } - unlimitedHeight - > - - ; - -EditSimpleLimitsBox.propTypes = { - editLimits: PropTypes.func.isRequired, - limits: PropTypes.func.isRequired -}; - -export default EditSimpleLimitsBox; diff --git a/src/components/Exercises/EditSimpleLimitsBox/index.js b/src/components/Exercises/EditSimpleLimitsBox/index.js deleted file mode 100644 index 5dfa24035..000000000 --- a/src/components/Exercises/EditSimpleLimitsBox/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './EditSimpleLimitsBox'; diff --git a/src/components/forms/EditSimpleLimits/EditEnvironmentLimitsFields.js b/src/components/forms/EditSimpleLimits/EditEnvironmentLimitsFields.js deleted file mode 100644 index 5bd091c04..000000000 --- a/src/components/forms/EditSimpleLimits/EditEnvironmentLimitsFields.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import { Field, FieldArray } from 'redux-form'; -import { TextField } from '../Fields'; -import EditHardwareGroupLimits from '../EditHardwareGroupLimits'; -import ResourceRenderer from '../../helpers/ResourceRenderer'; - -const EditEnvironmentLimitsFields = ({ - prefix, - i, - environments, - runtimeEnvironments -}) => { - const { environment, limits, referenceSolutionsEvaluations } = environments[ - i - ]; - const runtime = runtimeEnvironments - ? runtimeEnvironments.get(environment.runtimeEnvironmentId) - : null; - - return ( -
- - {runtime => ( -
-

{runtime.name}

-
    -
  • {runtime.language}
  • -
  • {runtime.platform}
  • -
  • {runtime.description}
  • -
-
- )} -
- - - } - /> - - -
- ); -}; - -EditEnvironmentLimitsFields.propTypes = { - prefix: PropTypes.string.isRequired, - i: PropTypes.number, - runtimeEnvironments: ImmutablePropTypes.map, - environments: PropTypes.arrayOf( - PropTypes.shape({ - referenceSolutionsEvaluations: PropTypes.object - }) - ).isRequired -}; - -export default EditEnvironmentLimitsFields; diff --git a/src/components/forms/EditSimpleLimits/EditEnvironmentSimpleLimitsForm.js b/src/components/forms/EditSimpleLimits/EditEnvironmentSimpleLimitsForm.js deleted file mode 100644 index 4deb80fa8..000000000 --- a/src/components/forms/EditSimpleLimits/EditEnvironmentSimpleLimitsForm.js +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { reduxForm } from 'redux-form'; - -import { LimitsField } from '../Fields'; -import SubmitButton from '../SubmitButton'; - -const EditEnvironmentLimitsForm = ({ - config, - envName, - onSubmit, - anyTouched, - submitting, - handleSubmit, - submitFailed: hasFailed, - submitSucceeded: hasSucceeded, - invalid, - asyncValidating, - setHorizontally, - setVertically, - setAll, - ...props -}) => -
- {config.tests.map(test => -
-

- {test.name} -

- -
-
- )} - -

- - ), - submitting: ( - - ), - success: ( - - ), - validating: ( - - ) - }} - /> -

-
; - -EditEnvironmentLimitsForm.propTypes = { - config: PropTypes.object.isRequired, - envName: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, - initialValues: PropTypes.object.isRequired, - values: PropTypes.object, - handleSubmit: PropTypes.func.isRequired, - anyTouched: PropTypes.bool, - submitting: PropTypes.bool, - submitFailed: PropTypes.bool, - submitSucceeded: PropTypes.bool, - invalid: PropTypes.bool, - asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - setVertically: PropTypes.func.isRequired, - setHorizontally: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired -}; - -const validate = ({ limits }) => { - const errors = {}; - - for (let test of Object.keys(limits)) { - const testErrors = {}; - const fields = limits[test]; - - if (!fields['memory'] || fields['memory'].length === 0) { - testErrors['memory'] = ( - - ); - } else if ( - Number(fields['memory']).toString() !== fields['memory'] || - Number(fields['memory']) <= 0 - ) { - testErrors['memory'] = ( - - ); - } - - if (!fields['time'] || fields['time'].length === 0) { - testErrors['time'] = ( - - ); - } else if ( - Number(fields['time']).toString() !== fields['time'] || - Number(fields['time']) <= 0 - ) { - testErrors['time'] = ( - - ); - } - - if (!fields['parallel'] || fields['parallel'].length === 0) { - testErrors['parallel'] = ( - - ); - } else if ( - Number(fields['parallel']).toString() !== fields['parallel'] || - Number(fields['parallel']) <= 0 - ) { - testErrors['parallel'] = ( - - ); - } - - if (testErrors.length > 0) { - errors[test] = testErrors; - } - } - - return errors; -}; - -export default reduxForm({ - form: 'editLimits', - validate -})(EditEnvironmentLimitsForm); diff --git a/src/components/forms/EditSimpleLimits/EditSimpleLimits.js b/src/components/forms/EditSimpleLimits/EditSimpleLimits.js deleted file mode 100644 index 29d79c59d..000000000 --- a/src/components/forms/EditSimpleLimits/EditSimpleLimits.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Row, Col, Label } from 'react-bootstrap'; - -import EditEnvironmentSimpleLimitsForm from './EditEnvironmentSimpleLimitsForm'; -import ResourceRenderer from '../../helpers/ResourceRenderer'; - -import styles from './styles.less'; - -const formName = id => `editEnvironmentSimpleLimits-${id}`; - -const fillInDefaultValuesWhereMissing = (testNames, limits) => - testNames.reduce( - (acc, test) => ({ - ...acc, - [test]: limits[test] || { memory: 0, 'wall-time': 0 } - }), - {} - ); - -const EditSimpleLimits = ({ - environments = [], - editLimits, - limits, - config, - setHorizontally, - setVertically, - setAll, - ...props -}) => - - {environments.map(({ id, name, platform, description }, i) => -
-
-

- {name} -

-

- {description} -

- - {limits => { - const envConfig = config.find(forEnv => forEnv.name === id); - return ( - test.name), - limits - ) - }} - form={formName(id)} - onSubmit={editLimits(id)} - setHorizontally={setHorizontally(formName(id), id)} - setVertically={setVertically(formName(id), id)} - setAll={setAll(formName(id), id)} - /> - ); - }} - -
- - )} - ; - -EditSimpleLimits.propTypes = { - config: PropTypes.array.isRequired, - environments: PropTypes.array, - editLimits: PropTypes.func.isRequired, - limits: PropTypes.func.isRequired, - setHorizontally: PropTypes.func.isRequired, - setVertically: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired -}; - -export default EditSimpleLimits; diff --git a/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js similarity index 60% rename from src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js rename to src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index db2e15d34..312bd8eee 100644 --- a/src/components/forms/EditSimpleLimits/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -12,17 +12,19 @@ import { encodeTestName, encodeEnvironmentId } from '../../../redux/modules/simpleLimits'; +import prettyMs from 'pretty-ms'; import styles from './styles.less'; const EditSimpleLimitsForm = ({ environments, - editLimits, tests, cloneHorizontally, cloneVertically, cloneAll, + handleSubmit, anyTouched, + dirty, submitting, submitFailed, submitSucceeded, @@ -36,18 +38,18 @@ const EditSimpleLimitsForm = ({ /> } unlimitedHeight - /* type={submitSucceeded ? 'success' : undefined} */ + success={submitSucceeded} + dirty={dirty} footer={
f} - /* asyncValidating={asyncValidating} */ + handleSubmit={handleSubmit} messages={{ submit: ( } @@ -95,7 +97,7 @@ const EditSimpleLimitsForm = ({ key={'th-' + environment.id} className={styles.limitsTableHeading} > - {environment.id} + {environment.name} )} @@ -141,15 +143,6 @@ const EditSimpleLimitsForm = ({ ); })} - - {/* */} )} @@ -159,11 +152,12 @@ const EditSimpleLimitsForm = ({ EditSimpleLimitsForm.propTypes = { tests: PropTypes.array.isRequired, environments: PropTypes.array, - editLimits: PropTypes.func.isRequired, cloneHorizontally: PropTypes.func.isRequired, cloneVertically: PropTypes.func.isRequired, cloneAll: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, anyTouched: PropTypes.bool, + dirty: PropTypes.bool, submitting: PropTypes.bool, submitFailed: PropTypes.bool, submitSucceeded: PropTypes.bool, @@ -172,78 +166,63 @@ EditSimpleLimitsForm.propTypes = { const validate = ({ limits }) => { const errors = {}; - /* - for (let test of Object.keys(limits)) { - const testErrors = {}; - const fields = limits[test]; + const maxSumTime = 300; // 5 minutes - if (!fields['memory'] || fields['memory'].length === 0) { - testErrors['memory'] = ( - - ); - } else if ( - Number(fields['memory']).toString() !== fields['memory'] || - Number(fields['memory']) <= 0 - ) { - testErrors['memory'] = ( - - ); - } + // Compute sum of wall times for each environment. + let sums = {}; + Object.keys(limits).forEach(test => + Object.keys(limits[test]).forEach(env => { + if (limits[test][env]['wall-time']) { + const val = Number(limits[test][env]['wall-time']); + if (!Number.isNaN(val) && val > 0) { + sums[env] = (sums[env] || 0) + val; + } + } + }) + ); - if (!fields['time'] || fields['time'].length === 0) { - testErrors['time'] = ( - - ); - } else if ( - Number(fields['time']).toString() !== fields['time'] || - Number(fields['time']) <= 0 - ) { - testErrors['time'] = ( - - ); - } - - if (!fields['parallel'] || fields['parallel'].length === 0) { - testErrors['parallel'] = ( - - ); - } else if ( - Number(fields['parallel']).toString() !== fields['parallel'] || - Number(fields['parallel']) <= 0 - ) { - testErrors['parallel'] = ( - - ); - } - - if (testErrors.length > 0) { - errors[test] = testErrors; + // Check if some environemnts have exceeded the limit ... + const limitsErrors = {}; + Object.keys(limits).forEach(test => { + const testsErrors = {}; + Object.keys(sums).forEach(env => { + if (sums[env] > maxSumTime) { + testsErrors[env] = { + 'wall-time': ( + + ) + }; + } + }); + if (Object.keys(testsErrors).length > 0) { + limitsErrors[test] = testsErrors; } + }); + if (Object.keys(limitsErrors).length > 0) { + errors['limits'] = limitsErrors; } -*/ + return errors; }; export default reduxForm({ form: 'editSimpleLimits', enableReinitialize: true, - keepDirtyOnReinitialize: true + keepDirtyOnReinitialize: true, + immutableProps: [ + 'environments', + 'tests', + 'cloneHorizontally', + 'cloneVertically', + 'cloneAll', + 'handleSubmit' + ], + validate })(EditSimpleLimitsForm); diff --git a/src/components/forms/EditSimpleLimits/index.js b/src/components/forms/EditSimpleLimitsForm/index.js similarity index 100% rename from src/components/forms/EditSimpleLimits/index.js rename to src/components/forms/EditSimpleLimitsForm/index.js diff --git a/src/components/forms/EditSimpleLimits/styles.less b/src/components/forms/EditSimpleLimitsForm/styles.less similarity index 100% rename from src/components/forms/EditSimpleLimits/styles.less rename to src/components/forms/EditSimpleLimitsForm/styles.less diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js index 240275f4e..9800fd49c 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -5,34 +5,15 @@ import { FormattedMessage } from 'react-intl'; import { Row, Col } from 'react-bootstrap'; import Icon from 'react-fontawesome'; -import { TextField } from '../Fields'; import FlatButton from '../../widgets/FlatButton'; import Confirm from '../../forms/Confirm'; +import LimitsValueField from './LimitsValueField'; import { prettyPrintBytes } from '../../helpers/stringFormatters'; import prettyMs from 'pretty-ms'; import styles from './EditSimpleLimitsField.less'; -const LimitsValueField = ({ input, prettyPrint, ...props }) => -
- - - ; - -LimitsValueField.propTypes = { - input: PropTypes.shape({ - value: PropTypes.any.isRequired - }).isRequired, - prettyPrint: PropTypes.func.isRequired -}; - const prettyPrintBytesWrap = value => Number.isNaN(Number(value)) ? '-' : prettyPrintBytes(Number(value) * 1024); @@ -87,8 +68,8 @@ const validateValue = (ranges, pretty) => value => { return undefined; }; -const validateMemory = validateValue(limitRanges.memory, prettyPrintBytes); -const validateTime = validateValue(limitRanges.time, prettyMs); +const validateMemory = validateValue(limitRanges.memory, prettyPrintBytesWrap); +const validateTime = validateValue(limitRanges.time, prettyPrintMsWrap); const EditSimpleLimitsField = ({ prefix, diff --git a/src/components/forms/Fields/KiloBytesTextField.js b/src/components/forms/Fields/KiloBytesTextField.js index 9829a4aff..d1d0f6195 100644 --- a/src/components/forms/Fields/KiloBytesTextField.js +++ b/src/components/forms/Fields/KiloBytesTextField.js @@ -1,69 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { HelpBlock, Col, Row, Confirm } from 'react-bootstrap'; +import { HelpBlock } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; -import Icon from 'react-fontawesome'; import { prettyPrintBytes } from '../../helpers/stringFormatters'; -import FlatButton from '../../widgets/FlatButton'; import TextField from './TextField'; +// !!! this component is no longer used in EditSimpleLimits, but it so may happen it will be recycled for the complex edit form... const KiloBytesTextField = ({ input, ...props }) =>
- {/* - - - } - > - - - - , - - } - > - - - - , - -  , - - {prettyPrintBytes(Number(input.value) * 1024)} - - - - - - - - - f} - question={ - - } - > - - - - />{' '} {prettyPrintBytes(Number(input.value) * 1024)} - */}
; KiloBytesTextField.propTypes = { diff --git a/src/components/forms/Fields/LimitsField.js b/src/components/forms/Fields/LimitsField.js index 13644f9cb..4c0f6f36d 100644 --- a/src/components/forms/Fields/LimitsField.js +++ b/src/components/forms/Fields/LimitsField.js @@ -8,6 +8,7 @@ import { KiloBytesTextField, SecondsTextField } from '../Fields'; import FlatButton from '../../widgets/FlatButton'; import Confirm from '../../forms/Confirm'; +// !!! this component is no longer used in EditSimpleLimits, but it so may happen it will be recycled for the complex edit form... const LimitsField = ({ label, prefix, diff --git a/src/components/forms/Fields/LimitsValueField.js b/src/components/forms/Fields/LimitsValueField.js new file mode 100644 index 000000000..1b915b807 --- /dev/null +++ b/src/components/forms/Fields/LimitsValueField.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { TextField } from '../Fields'; + +import styles from './EditSimpleLimitsField.less'; + +const LimitsValueField = ({ input, prettyPrint, ...props }) => + + + + ; + +LimitsValueField.propTypes = { + input: PropTypes.shape({ + value: PropTypes.any.isRequired + }).isRequired, + prettyPrint: PropTypes.func.isRequired +}; + +export default LimitsValueField; diff --git a/src/components/forms/Fields/SecondsTextField.js b/src/components/forms/Fields/SecondsTextField.js index 7249853d2..a033c045f 100644 --- a/src/components/forms/Fields/SecondsTextField.js +++ b/src/components/forms/Fields/SecondsTextField.js @@ -5,6 +5,7 @@ import TextField from './TextField'; import { HelpBlock } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; +// !!! this component is no longer used in EditSimpleLimits, but it so may happen it will be recycled for the complex edit form... const SecondsTextField = ({ input, ...props }) =>
diff --git a/src/components/forms/Fields/TextField.js b/src/components/forms/Fields/TextField.js index a8fe50293..9d4f2e3c2 100644 --- a/src/components/forms/Fields/TextField.js +++ b/src/components/forms/Fields/TextField.js @@ -11,7 +11,7 @@ import { const TextField = ({ input: { value, ...input }, - meta: { touched, error }, + meta: { touched, dirty, error }, type = 'text', label, groupClassName = '', @@ -19,7 +19,7 @@ const TextField = ({ }) => @@ -37,12 +37,7 @@ const TextField = ({ /> {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} } ; @@ -57,6 +52,7 @@ TextField.propTypes = { }).isRequired, meta: PropTypes.shape({ touched: PropTypes.bool, + dirty: PropTypes.bool, error: PropTypes.any }).isRequired, label: PropTypes.oneOfType([ diff --git a/src/components/forms/Fields/index.js b/src/components/forms/Fields/index.js index 176881806..4981a83c6 100644 --- a/src/components/forms/Fields/index.js +++ b/src/components/forms/Fields/index.js @@ -3,6 +3,7 @@ export { default as EmailField } from './EmailField'; export { default as DatetimeField } from './DatetimeField'; export { default as MarkdownTextAreaField } from './MarkdownTextAreaField'; export { default as EditSimpleLimitsField } from './EditSimpleLimitsField'; +export { default as LimitsValueField } from './LimitsValueField'; export { default as LimitsField } from './LimitsField'; export { default as PasswordField } from './PasswordField'; export { default as PasswordStrength } from './PasswordStrength'; diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 79a0ddd80..786a81a23 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -284,6 +284,10 @@ export const transformAndSendConfigValues = ( return setConfig({ config: envs }); }; +/** + * Assemble data from all simpleLimits environments into one table. + * Also ensures proper encoding for environment IDs and test names, which are used as keys. + */ export const getLimitsInitValues = ( limits, tests, @@ -296,8 +300,8 @@ export const getLimitsInitValues = ( const testId = encodeTestName(test.name); res[testId] = {}; environments.forEach(environment => { - const envId = encodeEnvironmentId(environment.id); // the name can be anything, but it must be compatible with redux-form - const lim = limits.getIn([ + const envId = encodeEnvironmentId(environment.id); + let lim = limits.getIn([ endpointDisguisedAsIdFactory({ exerciseId, runtimeEnvironmentId: environment.id @@ -305,15 +309,40 @@ export const getLimitsInitValues = ( 'data', test.name ]); - res[testId][envId] = lim - ? lim.toJS() - : { - // default - memory: 0, - 'wall-time': 0 - }; + if (lim) { + lim = lim.toJS(); + } + + res[testId][envId] = { + memory: lim ? String(lim.memory) : '0', + 'wall-time': lim ? String(lim['wall-time']) : '0' + }; }); }); return { limits: res }; }; + +/** + * Transform form data and pass them to dispatching function. + * The data have to be re-assembled, since they use different format and keys are encoded. + * The dispatching function is invoked for every environment and all promise is returned. + */ +export const transformAndSendLimitsValues = ( + formData, + tests, + runtimeEnvironments, + editEnvironmentSimpleLimits +) => + Promise.all( + runtimeEnvironments.map(environment => { + const envId = encodeEnvironmentId(environment.id); + const data = { + limits: tests.reduce((acc, test) => { + acc[test.name] = formData.limits[encodeTestName(test.name)][envId]; + return acc; + }, {}) + }; + return editEnvironmentSimpleLimits(environment.id, data); + }) + ); diff --git a/src/locales/cs.json b/src/locales/cs.json index 03ba257be..5b89993ec 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -1,5 +1,10 @@ { "app.EditEnvironmentLimitsForm.cloneAll.yesNoQuestion": "Do you really want to use these limits for all the tests of all runtime environments?", + "app.EditLimitsForm.cloneAll.yesNoQuestion": "Do you really want to use these limits for all the tests of all runtime environments? Pleae note, that individual environments have different performance characteristics.", + "app.EditLimitsForm.cloneHorizontal.yesNoQuestion": "Do you really want to use these limits for all runtime environments of this test? Pleae note, that individual environments have different performance characteristics.", + "app.EditSimpleLimitsForm.validation.NaN": "Given value is not a number.", + "app.EditSimpleLimitsForm.validation.tooHigh": "Given value exceeds the recommended maximum ({max}).", + "app.EditSimpleLimitsForm.validation.tooLow": "Given value is below the recommended minimum ({min}).", "app.acceptSolution.accepted": "Zrušit jako finální", "app.acceptSolution.notAccepted": "Akceptovat jako finální", "app.addLicence.addLicenceTitle": "Přidat novou licenci", @@ -219,12 +224,20 @@ "app.editEnvironmentLimitsForm.validation.parallel.mustBePositive": "You must set the limit for the number of parallel processes to a positive number.", "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", + "app.editEnvironmentSimpleForm.failed": "Saving failed. Please try again later.", + "app.editEnvironmentSimpleForm.submit": "Change configuration", + "app.editEnvironmentSimpleForm.submitting": "Saving configuration ...", + "app.editEnvironmentSimpleForm.success": "Configuration was changed.", + "app.editEnvironmentSimpleForm.validation.environments": "Please add at least one runtime environment.", "app.editExercise.deleteExercise": "Smazat úlohu", "app.editExercise.deleteExerciseWarning": "Smazání úlohy odstraní všechna studentská řešení a všechna zadání této úlohy.", "app.editExercise.description": "Změna nastavení úlohy", + "app.editExercise.editConfig": "Edit exercise configuration", "app.editExercise.editEnvironmentConfig": "Upravit konfigurace prostředí", + "app.editExercise.editEnvironments": "Edit runtime environments", "app.editExercise.editScoreConfig": "Edit score configurations", "app.editExercise.editTestConfig": "Upravit konfigurace", + "app.editExercise.editTests": "Edit tests", "app.editExercise.title": "Změna nastavení úlohy", "app.editExerciseConfig.description": "Change exercise configuration", "app.editExerciseConfig.title": "Edit exercise config", @@ -264,6 +277,29 @@ "app.editExerciseForm.validation.noLocalizedText": "Prosíme přidejte alespoň jeden lokalizovaný text popisující tuto úlohu.", "app.editExerciseForm.validation.sameLocalizedTexts": "Je vyplněno více jazykových variant pro jednu lokalizaci. Prosím ujistěte se, že lokalizace jsou unikátní.", "app.editExerciseForm.validation.versionDiffers": "Někdo změnil tuto úlohu v průběhu její editace. Prosíme obnovte si tuto stránku a proveďte své změny znovu.", + "app.editExerciseSimpleConfigForm.submit": "Change configuration", + "app.editExerciseSimpleConfigForm.submitting": "Saving configuration ...", + "app.editExerciseSimpleConfigForm.success": "Configuration was changed.", + "app.editExerciseSimpleConfigForm.validation.customJudge": "Please select the custom judge binary for this test or use one of the standard judges instead.", + "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Please fill the expected output file.", + "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Input files are not properly paired with their names. Please make sure each file has a name.", + "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Please select the judge type for this test.", + "app.editExerciseSimpleConfigForm.validation.outputFile": "Please fill the name of the output file or use standard input instead.", + "app.editExerciseSimpleConfigTests.customJudgeBinary": "Custom judge binary:", + "app.editExerciseSimpleConfigTests.executionArguments": "Execution arguments:", + "app.editExerciseSimpleConfigTests.executionTitle": "Execution", + "app.editExerciseSimpleConfigTests.expectedOutput": "Expected output:", + "app.editExerciseSimpleConfigTests.inputFilesActual": "Input file:", + "app.editExerciseSimpleConfigTests.inputFilesRename": "Renamed file name:", + "app.editExerciseSimpleConfigTests.inputStdin": "Stdin:", + "app.editExerciseSimpleConfigTests.inputTitle": "Input", + "app.editExerciseSimpleConfigTests.judgeArgs": "Judge arguments:", + "app.editExerciseSimpleConfigTests.judgeBinary": "Judge binary:", + "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", + "app.editExerciseSimpleConfigTests.outputFile": "Output file:", + "app.editExerciseSimpleConfigTests.outputTitle": "Output", + "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", + "app.editExerciseSimpleConfigTests.useOutfile": "Use output file instead of stdout", "app.editGroup.cannotDeleteRootGroup": "Toto je primární skupina a jako taková nemůže být smazána.", "app.editGroup.deleteGroup": "Smazat skupinu", "app.editGroup.deleteGroupWarning": "Smazání skupiny odstraní také všechny podskupiny, skupinové úlohy a zadané úlohy včetně studentských řešení.", @@ -324,6 +360,25 @@ "app.editScoreConfigForm.submit": "Change configuration", "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", + "app.editSimpleLimitsForm.failed": "Cannot save the exercise limits. Please try again later.", + "app.editSimpleLimitsForm.submit": "Save Limits", + "app.editSimpleLimitsForm.submitting": "Saving Limits ...", + "app.editSimpleLimitsForm.success": "Limits Saved.", + "app.editSimpleLimitsForm.validating": "Validating...", + "app.editSimpleLimitsForm.validation.timeSum": "The sum of time limits ({sum}) exceeds allowed maximum ({max}).", + "app.editTestsForm.failed": "Saving failed. Please try again later.", + "app.editTestsForm.isUniform": "Using uniform point distribution for all tests", + "app.editTestsForm.submit": "Change configuration", + "app.editTestsForm.submitting": "Saving configuration ...", + "app.editTestsForm.success": "Configuration was changed.", + "app.editTestsForm.validation.testName": "Please fill test name.", + "app.editTestsForm.validation.testNameTaken": "This name is taken, please fill different one.", + "app.editTestsForm.validation.testWeight": "Test weight must be positive integer.", + "app.editTestsForm.validation.testWeightEmpty": "Please fill test weight.", + "app.editTestsTest.add": "Add test", + "app.editTestsTest.name": "Test name:", + "app.editTestsTest.remove": "Remove", + "app.editTestsTest.weight": "Test weight:", "app.editUser.description": "Upravit nastavení uživatele", "app.editUser.title": "Upravit uživatelský profil", "app.editUserProfile.degreesAfterName": "Tituly za jménem:", @@ -927,5 +982,14 @@ "app.usersName.loading": "Načítání ...", "app.usersName.notVerified.title": "Tento účet nemá ověřenou emailovou adresu.", "app.usersStats.description": "Body získané ve skupině {name}.", - "app.usersname.notVerified.description": "Tento uživatel si neověřil svou emailovou adresu přes aktivační odkaz, který mu byl na tuto adresu zaslán." + "app.usersname.notVerified.description": "Tento uživatel si neověřil svou emailovou adresu přes aktivační odkaz, který mu byl na tuto adresu zaslán.", + "diff": "Diff", + "recodex-judge-float": "Floating point judge", + "recodex-judge-float-newline": "Floating judge ignoring newlines", + "recodex-judge-normal": "Normal judge", + "recodex-judge-normal-newline": "Normal judge ignoring newlines", + "recodex-judge-shuffle": "Shuffle judge", + "recodex-judge-shuffle-all": "Shuffle judge ignoring all", + "recodex-judge-shuffle-newline": "Shuffle judge ignoring newlines", + "recodex-judge-shuffle-rows": "Shuffle judge ignoring rows" } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 95fa7ebb7..ca29ff78a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,5 +1,10 @@ { "app.EditEnvironmentLimitsForm.cloneAll.yesNoQuestion": "Do you really want to use these limits for all the tests of all runtime environments?", + "app.EditLimitsForm.cloneAll.yesNoQuestion": "Do you really want to use these limits for all the tests of all runtime environments? Pleae note, that individual environments have different performance characteristics.", + "app.EditLimitsForm.cloneHorizontal.yesNoQuestion": "Do you really want to use these limits for all runtime environments of this test? Pleae note, that individual environments have different performance characteristics.", + "app.EditSimpleLimitsForm.validation.NaN": "Given value is not a number.", + "app.EditSimpleLimitsForm.validation.tooHigh": "Given value exceeds the recommended maximum ({max}).", + "app.EditSimpleLimitsForm.validation.tooLow": "Given value is below the recommended minimum ({min}).", "app.acceptSolution.accepted": "Revoke as Final", "app.acceptSolution.notAccepted": "Accept as Final", "app.addLicence.addLicenceTitle": "Add new licence", @@ -219,12 +224,20 @@ "app.editEnvironmentLimitsForm.validation.parallel.mustBePositive": "You must set the limit for the number of parallel processes to a positive number.", "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", + "app.editEnvironmentSimpleForm.failed": "Saving failed. Please try again later.", + "app.editEnvironmentSimpleForm.submit": "Change configuration", + "app.editEnvironmentSimpleForm.submitting": "Saving configuration ...", + "app.editEnvironmentSimpleForm.success": "Configuration was changed.", + "app.editEnvironmentSimpleForm.validation.environments": "Please add at least one runtime environment.", "app.editExercise.deleteExercise": "Delete the exercise", "app.editExercise.deleteExerciseWarning": "Deleting an exercise will remove all the students submissions and all assignments.", "app.editExercise.description": "Change exercise settings", + "app.editExercise.editConfig": "Edit exercise configuration", "app.editExercise.editEnvironmentConfig": "Edit environment configurations", + "app.editExercise.editEnvironments": "Edit runtime environments", "app.editExercise.editScoreConfig": "Edit score configurations", "app.editExercise.editTestConfig": "Edit configurations", + "app.editExercise.editTests": "Edit tests", "app.editExercise.title": "Edit exercise settings", "app.editExerciseConfig.description": "Change exercise configuration", "app.editExerciseConfig.title": "Edit exercise config", @@ -264,6 +277,29 @@ "app.editExerciseForm.validation.noLocalizedText": "Please add at least one localized text describing the exercise.", "app.editExerciseForm.validation.sameLocalizedTexts": "There are more language variants with the same locale. Please make sure locales are unique.", "app.editExerciseForm.validation.versionDiffers": "Somebody has changed the exercise while you have been editing it. Please reload the page and apply your changes once more.", + "app.editExerciseSimpleConfigForm.submit": "Change configuration", + "app.editExerciseSimpleConfigForm.submitting": "Saving configuration ...", + "app.editExerciseSimpleConfigForm.success": "Configuration was changed.", + "app.editExerciseSimpleConfigForm.validation.customJudge": "Please select the custom judge binary for this test or use one of the standard judges instead.", + "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Please fill the expected output file.", + "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Input files are not properly paired with their names. Please make sure each file has a name.", + "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Please select the judge type for this test.", + "app.editExerciseSimpleConfigForm.validation.outputFile": "Please fill the name of the output file or use standard input instead.", + "app.editExerciseSimpleConfigTests.customJudgeBinary": "Custom judge binary:", + "app.editExerciseSimpleConfigTests.executionArguments": "Execution arguments:", + "app.editExerciseSimpleConfigTests.executionTitle": "Execution", + "app.editExerciseSimpleConfigTests.expectedOutput": "Expected output:", + "app.editExerciseSimpleConfigTests.inputFilesActual": "Input file:", + "app.editExerciseSimpleConfigTests.inputFilesRename": "Renamed file name:", + "app.editExerciseSimpleConfigTests.inputStdin": "Stdin:", + "app.editExerciseSimpleConfigTests.inputTitle": "Input", + "app.editExerciseSimpleConfigTests.judgeArgs": "Judge arguments:", + "app.editExerciseSimpleConfigTests.judgeBinary": "Judge binary:", + "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", + "app.editExerciseSimpleConfigTests.outputFile": "Output file:", + "app.editExerciseSimpleConfigTests.outputTitle": "Output", + "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", + "app.editExerciseSimpleConfigTests.useOutfile": "Use output file instead of stdout", "app.editGroup.cannotDeleteRootGroup": "This is a so-called root group and it cannot be deleted.", "app.editGroup.deleteGroup": "Delete the group", "app.editGroup.deleteGroupWarning": "Deleting a group will remove all the subgroups, the students submissions and all the assignments and the submissions of the students.", @@ -324,6 +360,25 @@ "app.editScoreConfigForm.submit": "Change configuration", "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", + "app.editSimpleLimitsForm.failed": "Cannot save the exercise limits. Please try again later.", + "app.editSimpleLimitsForm.submit": "Save Limits", + "app.editSimpleLimitsForm.submitting": "Saving Limits ...", + "app.editSimpleLimitsForm.success": "Limits Saved.", + "app.editSimpleLimitsForm.validating": "Validating...", + "app.editSimpleLimitsForm.validation.timeSum": "The sum of time limits ({sum}) exceeds allowed maximum ({max}).", + "app.editTestsForm.failed": "Saving failed. Please try again later.", + "app.editTestsForm.isUniform": "Using uniform point distribution for all tests", + "app.editTestsForm.submit": "Change configuration", + "app.editTestsForm.submitting": "Saving configuration ...", + "app.editTestsForm.success": "Configuration was changed.", + "app.editTestsForm.validation.testName": "Please fill test name.", + "app.editTestsForm.validation.testNameTaken": "This name is taken, please fill different one.", + "app.editTestsForm.validation.testWeight": "Test weight must be positive integer.", + "app.editTestsForm.validation.testWeightEmpty": "Please fill test weight.", + "app.editTestsTest.add": "Add test", + "app.editTestsTest.name": "Test name:", + "app.editTestsTest.remove": "Remove", + "app.editTestsTest.weight": "Test weight:", "app.editUser.description": "Edit user's profile", "app.editUser.title": "Edit user's profile", "app.editUserProfile.degreesAfterName": "Degrees after name:", @@ -927,5 +982,14 @@ "app.usersName.loading": "Loading ...", "app.usersName.notVerified.title": "This account does not have a verified email address yet.", "app.usersStats.description": "Points gained from {name}.", - "app.usersname.notVerified.description": "This user has not verified his/her email address via an activation link he has received to his email address." + "app.usersname.notVerified.description": "This user has not verified his/her email address via an activation link he has received to his email address.", + "diff": "Diff", + "recodex-judge-float": "Floating point judge", + "recodex-judge-float-newline": "Floating judge ignoring newlines", + "recodex-judge-normal": "Normal judge", + "recodex-judge-normal-newline": "Normal judge ignoring newlines", + "recodex-judge-shuffle": "Shuffle judge", + "recodex-judge-shuffle-all": "Shuffle judge ignoring all", + "recodex-judge-shuffle-newline": "Shuffle judge ignoring newlines", + "recodex-judge-shuffle-rows": "Shuffle judge ignoring rows" } \ No newline at end of file diff --git a/src/pages/EditExerciseConfig/EditExerciseConfig.js b/src/pages/EditExerciseConfig/EditExerciseConfig.js index 373e8b19a..2e09ee84e 100644 --- a/src/pages/EditExerciseConfig/EditExerciseConfig.js +++ b/src/pages/EditExerciseConfig/EditExerciseConfig.js @@ -14,7 +14,6 @@ import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; import EditExerciseConfigForm from '../../components/forms/EditExerciseConfigForm/EditExerciseConfigForm'; import EditEnvironmentConfigForm from '../../components/forms/EditEnvironmentConfigForm'; import EditScoreConfigForm from '../../components/forms/EditScoreConfigForm'; -import EditSimpleLimitsBox from '../../components/Exercises/EditSimpleLimitsBox'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer'; @@ -22,10 +21,7 @@ import { fetchExerciseIfNeeded } from '../../redux/modules/exercises'; import { fetchPipelines } from '../../redux/modules/pipelines'; import { fetchExerciseEnvironmentSimpleLimitsIfNeeded, - editEnvironmentSimpleLimits, - setHorizontally, - setVertically, - setAll + editEnvironmentSimpleLimits } from '../../redux/modules/simpleLimits'; import { fetchExerciseConfigIfNeeded, @@ -97,9 +93,6 @@ class EditExerciseConfig extends Component { editEnvironmentSimpleLimits, pipelines, limits, - setHorizontally, - setVertically, - setAll, editScoreConfig, superadmin, intl: { locale } @@ -210,6 +203,7 @@ class EditExerciseConfig extends Component { exercise={exercise} pipelines={pipelines} />} + {/* Limit editation was completely redefined in simple form. + /> */}
} @@ -246,9 +240,6 @@ EditExerciseConfig.propTypes = { pipelines: ImmutablePropTypes.map, links: PropTypes.object.isRequired, limits: PropTypes.func.isRequired, - setHorizontally: PropTypes.func.isRequired, - setVertically: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired, editScoreConfig: PropTypes.func.isRequired, superadmin: PropTypes.bool.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired @@ -283,23 +274,6 @@ export default injectIntl( editEnvironmentSimpleLimits(exerciseId, runtimeEnvironmentId, data) ), setConfig: data => dispatch(setExerciseConfig(exerciseId, data)), - setHorizontally: (formName, runtimeEnvironmentId) => testName => () => - dispatch( - setHorizontally( - formName, - exerciseId, - runtimeEnvironmentId, - testName - ) - ), - setVertically: (formName, runtimeEnvironmentId) => testName => () => - dispatch( - setVertically(formName, exerciseId, runtimeEnvironmentId, testName) - ), - setAll: (formName, runtimeEnvironmentId) => testName => () => - dispatch( - setAll(formName, exerciseId, runtimeEnvironmentId, testName) - ), editScoreConfig: data => dispatch(setScoreConfig(exerciseId, data)) }) )(EditExerciseConfig) diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index 4df9168ba..2d1a37b99 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -9,7 +9,7 @@ import Page from '../../components/layout/Page'; import Box from '../../components/widgets/Box'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; -import EditSimpleLimitsForm from '../../components/forms/EditSimpleLimits/EditSimpleLimitsForm'; +import EditSimpleLimitsForm from '../../components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer'; import EditTestsForm from '../../components/forms/EditTestsForm'; import EditExerciseSimpleConfigForm from '../../components/forms/EditExerciseSimpleConfigForm'; @@ -61,30 +61,10 @@ import { transformAndSendTestsValues, getSimpleConfigInitValues, transformAndSendConfigValues, - getLimitsInitValues + getLimitsInitValues, + transformAndSendLimitsValues } from '../../helpers/exerciseSimpleForm'; -const getLimitsInitValuesOld = (limits, tests, environments) => { - let res = {}; - - tests.forEach(test => { - const testId = 'test' + btoa(test.name); // the name can be anything, but it must be compatible with redux-form - res[testId] = {}; - environments.forEach(environment => { - const envId = 'env' + btoa(environment.id); // the name can be anything, but it must be compatible with redux-form - res[testId][envId] = - limits[environment.id] && limits[environment.id][test.name] - ? limits[environment.id][test.name] - : { - memory: 0, - 'wall-time': 0 - }; - }); - }); - - return { limits: res }; -}; - class EditExerciseSimpleConfig extends Component { componentWillMount = () => this.props.loadAsync(); componentWillReceiveProps = props => { @@ -293,10 +273,18 @@ class EditExerciseSimpleConfig extends Component { - + {tests => + transformAndSendLimitsValues( + data, + tests, + exercise.runtimeEnvironments, + editEnvironmentSimpleLimits + )} environments={exercise.runtimeEnvironments} tests={tests.sort((a, b) => a.name.localeCompare(b.name, locale) @@ -312,22 +300,6 @@ class EditExerciseSimpleConfig extends Component { cloneAll={cloneAll} />} - {/* - - {config => -
- -
-
- */} } @@ -382,7 +354,7 @@ export default injectIntl( (dispatch, { params: { exerciseId } }) => ({ loadAsync: () => EditExerciseSimpleConfig.loadAsync({ exerciseId }, dispatch), - editEnvironmentSimpleLimits: runtimeEnvironmentId => data => + editEnvironmentSimpleLimits: (runtimeEnvironmentId, data) => dispatch( editEnvironmentSimpleLimits(exerciseId, runtimeEnvironmentId, data) ), diff --git a/src/redux/middleware/loggerMiddleware.js b/src/redux/middleware/loggerMiddleware.js index 881d08fc8..3a2ff8c8f 100644 --- a/src/redux/middleware/loggerMiddleware.js +++ b/src/redux/middleware/loggerMiddleware.js @@ -1,25 +1,28 @@ -const middleware = isDev => store => next => action => { - /* eslint no-console: ["error", { allow: ["log", "error"] }] */ - let verbose = true; +const middleware = ( + noDOM, + verbose, + fullException +) => store => next => action => { + /* eslint no-console: ["error", { allow: ["log", "error", "debug"] }] */ var actionType = action.type; if (verbose) { - console.log('Starting ' + actionType); - console.log(action); - } else if (isDev) { - console.log(actionType); + console.debug('Starting ' + actionType); + console.debug(action); + } else if (noDOM) { + console.debug(actionType); } try { var res = next(action); } catch (e) { - console.error('Exception thrown when processing action ' + actionType); - if (verbose) console.error(e); + console.log('Exception thrown when processing action ' + actionType); + if (fullException) console.error(e); throw e; } if (verbose) { - console.log('State After Action ' + actionType); - console.log(store.getState().groups); - console.log('--------------------'); + console.debug('State After Action ' + actionType); + console.debug(store.getState().groups); + console.debug('--------------------'); } return res; diff --git a/src/redux/modules/simpleLimits.js b/src/redux/modules/simpleLimits.js index 460668066..210cd1d47 100644 --- a/src/redux/modules/simpleLimits.js +++ b/src/redux/modules/simpleLimits.js @@ -49,27 +49,6 @@ export const editEnvironmentSimpleLimits = ( data ); -/* -const isSimpleLimitsForm = ({ registeredFields }, testName) => - registeredFields && - registeredFields.hasOwnProperty(`limits.${testName}.wall-time`) && - registeredFields.hasOwnProperty(`limits.${testName}.memory`); - -const getAllSimpleLimitsFormNames = ({ form }, testName) => - Object.keys(form).filter(name => isSimpleLimitsForm(form[name], testName)); - -const getAllTestNames = ({ form }, formName) => - Object.keys(form[formName].registeredFields) - .map(name => { - const firstDot = name.indexOf('.'); - const lastDot = name.lastIndexOf('.'); - return name.substring(firstDot + 1, lastDot); - }) - .reduce((acc, name) => (acc.indexOf(name) >= 0 ? acc : [...acc, name]), []); - -const field = testName => `limits.${testName}`; -*/ - /* * Special functions for cloning buttons */ diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js index 143776990..f3f35a964 100644 --- a/src/redux/selectors/simpleLimits.js +++ b/src/redux/selectors/simpleLimits.js @@ -1,24 +1,9 @@ import { createSelector } from 'reselect'; -import { Map } from 'immutable'; import { getExerciseRuntimeEnvironments } from './exercises'; import { endpointDisguisedAsIdFactory } from '../modules/simpleLimits'; const getLimits = state => state.simpleLimits; const EMPTY_OBJ = {}; -const EMPTY_MAP = Map(); - -/* -export const simpleLimitsSelector = (exerciseId, runtimeEnvironmentId) => - createSelector(getLimits, limits => - limits.getIn([ - 'resources', - endpointDisguisedAsIdFactory({ - exerciseId, - runtimeEnvironmentId - }) - ]) - ); -*/ export const simpleLimitsSelector = createSelector(getLimits, limits => limits.get('resources') From a6dc91fcd64beaf533495c07353c40497a5b669d Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 7 Dec 2017 01:07:11 +0100 Subject: [PATCH 22/47] Changing logger middleware to get configuration from .env. --- .env-sample | 8 ++++++++ config/webpack.config.js | 4 +++- src/redux/store.js | 9 ++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.env-sample b/.env-sample index ebbf2820f..1de723d46 100644 --- a/.env-sample +++ b/.env-sample @@ -10,3 +10,11 @@ ALLOW_NORMAL_REGISTRATION=true ALLOW_LDAP_REGISTRATION=false ALLOW_CAS_REGISTRATION=true + +# LOGGER_MIDDLEWARE is active only if NODE_ENV=development + +# verbose debug action logging +LOGGER_MIDDLEWARE_VERBOSE=false + +# log complete dump of exceptions +LOGGER_MIDDLEWARE_EXCEPTIONS=true diff --git a/config/webpack.config.js b/config/webpack.config.js index 4b8819bd4..890fb04a3 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -65,7 +65,9 @@ module.exports = { "'" + process.env.ALLOW_NORMAL_REGISTRATION + "'", ALLOW_LDAP_REGISTRATION: "'" + process.env.ALLOW_LDAP_REGISTRATION + "'", - ALLOW_CAS_REGISTRATION: "'" + process.env.ALLOW_CAS_REGISTRATION + "'" + ALLOW_CAS_REGISTRATION: "'" + process.env.ALLOW_CAS_REGISTRATION + "'", + LOGGER_MIDDLEWARE_VERBOSE: "'" + process.env.LOGGER_MIDDLEWARE_VERBOSE + "'", + LOGGER_MIDDLEWARE_EXCEPTIONS: "'" + process.env.LOGGER_MIDDLEWARE_EXCEPTIONS + "'", }, gitRevision: { VERSION: JSON.stringify(gitRevisionPlugin.version()), diff --git a/src/redux/store.js b/src/redux/store.js index 1e0fbcdfa..36af57796 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -51,7 +51,14 @@ const composeEnhancers = REDUX_DEV_SERVER_PORT const dev = history => composeEnhancers( compose( - applyMiddleware(...getMiddleware(history), loggerMiddleware(!canUseDOM)), + applyMiddleware( + ...getMiddleware(history), + loggerMiddleware( + !canUseDOM, + process.env.LOGGER_MIDDLEWARE_VERBOSE === 'true', + process.env.LOGGER_MIDDLEWARE_EXCEPTIONS === 'true' + ) + ), !REDUX_DEV_SERVER_PORT && canUseDOM && window.devToolsExtension // if no server is in place and dev tools are available, use them ? window.devToolsExtension() : f => f From 0158a94e93690821fb2fc5ad176d6db45bc0c37d Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 7 Dec 2017 11:33:04 +0100 Subject: [PATCH 23/47] Adapt to changed API format --- .../EditExerciseSimpleConfigTest.js | 2 +- src/helpers/exerciseSimpleForm.js | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index ecfd2637a..4125e0a2e 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -64,7 +64,7 @@ const EditExerciseSimpleConfigTest = ({ .sort((a, b) => a.name.localeCompare(b.name, intl.locale)) .filter((item, pos, arr) => arr.indexOf(item) === pos) .map(data => ({ - key: data.hashName, + key: data.name, name: data.name })) ); diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 786a81a23..d8480d9ef 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -175,12 +175,11 @@ export const transformAndSendConfigValues = ( let testVars = []; for (const test of formData.config) { let variables = []; - let producesFiles = false; variables.push({ name: 'custom-judge', type: 'remote-file', - value: test.customJudgeBinary + value: test.useCustomJudge ? test.customJudgeBinary : '' }); variables.push({ name: 'expected-output', @@ -207,13 +206,12 @@ export const transformAndSendConfigValues = ( type: 'string[]', value: test.runArgs }); - if (test.outputFile !== '') { + if (test.useOutFile) { variables.push({ name: 'actual-output', type: 'file[]', value: test.outputFile }); - producesFiles = true; } let inputFiles = []; @@ -238,7 +236,7 @@ export const transformAndSendConfigValues = ( testVars.push({ name: test.name, variables: variables, - producesFiles: producesFiles + producesFiles: test.useOutFile }); } @@ -246,7 +244,7 @@ export const transformAndSendConfigValues = ( for (const environment of environments) { const envId = environment.runtimeEnvironmentId; const envPipelines = pipelines.filter( - pipeline => pipeline.runtimeEnvironmentId === envId + pipeline => pipeline.runtimeEnvironmentIds.indexOf(envId) >= 0 ); let tests = []; From 264efdd6b73fb8d7fca29a5eb9a7f4308b68add1 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 7 Dec 2017 12:35:29 +0100 Subject: [PATCH 24/47] Updating names of judges. --- .../EditExerciseSimpleConfigTest.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index ecfd2637a..102cc0118 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -15,39 +15,39 @@ import { const messages = defineMessages({ normal: { id: 'recodex-judge-normal', - defaultMessage: 'Normal judge' + defaultMessage: 'Token judge' }, float: { id: 'recodex-judge-float', - defaultMessage: 'Floating point judge' + defaultMessage: 'Float-numbers judge' }, normalNewline: { id: 'recodex-judge-normal-newline', - defaultMessage: 'Normal judge ignoring newlines' + defaultMessage: 'Token judge (ignoring ends of lines)' }, floatNewline: { id: 'recodex-judge-float-newline', - defaultMessage: 'Floating judge ignoring newlines' + defaultMessage: 'Float-numbers judge (ignoring ends of lines)' }, shuffle: { id: 'recodex-judge-shuffle', - defaultMessage: 'Shuffle judge' + defaultMessage: 'Unordered-tokens judge' }, shuffleRows: { id: 'recodex-judge-shuffle-rows', - defaultMessage: 'Shuffle judge ignoring rows' + defaultMessage: 'Unordered-rows judge' }, shuffleAll: { id: 'recodex-judge-shuffle-all', - defaultMessage: 'Shuffle judge ignoring all' + defaultMessage: 'Unordered-tokens-and-rows judge' }, shuffleNewline: { id: 'recodex-judge-shuffle-newline', - defaultMessage: 'Shuffle judge ignoring newlines' + defaultMessage: 'Unordered-tokens judge (ignoring ends of lines)' }, diff: { id: 'diff', - defaultMessage: 'Diff' + defaultMessage: 'Binary-safe judge' } }); From a8bb4a1d4320b340c057a269b8e5eb0d4e4ef21f Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 7 Dec 2017 12:39:07 +0100 Subject: [PATCH 25/47] New items for assignment synchronization --- .../AssignmentSync/AssignmentSync.js | 116 ++++++++++++++ .../Assignment/AssignmentSync/index.js | 1 + .../forms/EditSimpleLimitsForm/index.js | 2 +- src/pages/Assignment/Assignment.js | 103 +----------- .../EditExerciseConfig/EditExerciseConfig.js | 4 +- test/redux/simpleLimits.js | 149 ------------------ 6 files changed, 126 insertions(+), 249 deletions(-) create mode 100644 src/components/Assignments/Assignment/AssignmentSync/AssignmentSync.js create mode 100644 src/components/Assignments/Assignment/AssignmentSync/index.js delete mode 100644 test/redux/simpleLimits.js diff --git a/src/components/Assignments/Assignment/AssignmentSync/AssignmentSync.js b/src/components/Assignments/Assignment/AssignmentSync/AssignmentSync.js new file mode 100644 index 000000000..2304db7fe --- /dev/null +++ b/src/components/Assignments/Assignment/AssignmentSync/AssignmentSync.js @@ -0,0 +1,116 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Row, Col, Alert } from 'react-bootstrap'; + +import Button from '../../../widgets/FlatButton'; + +const AssignmentSync = ({ syncInfo, exerciseSync }) => + !syncInfo.exerciseConfig.upToDate || + !syncInfo.configurationType.upToDate || + !syncInfo.exerciseEnvironmentConfigs.upToDate || + !syncInfo.hardwareGroups.upToDate || + !syncInfo.localizedTexts.upToDate || + !syncInfo.limits.upToDate || + !syncInfo.scoreConfig.upToDate || + !syncInfo.scoreCalculator.upToDate || + !syncInfo.exerciseTests.upToDate + ? +
+ +

+ +

+
+ +
    + {!syncInfo.exerciseConfig.upToDate && +
  • + +
  • } + {!syncInfo.configurationType.upToDate && +
  • + +
  • } + {!syncInfo.exerciseEnvironmentConfigs.upToDate && +
  • + +
  • } + {!syncInfo.hardwareGroups.upToDate && +
  • + +
  • } + {!syncInfo.localizedTexts.upToDate && +
  • + +
  • } + {!syncInfo.limits.upToDate && +
  • + +
  • } + {!syncInfo.scoreConfig.upToDate && +
  • + +
  • } + {!syncInfo.scoreCalculator.upToDate && +
  • + +
  • } + {!syncInfo.exerciseTests.upToDate && +
  • + +
  • } +
+
+

+ +

+
+ + + :
; + +AssignmentSync.propTypes = { + syncInfo: PropTypes.object.isRequired, + exerciseSync: PropTypes.func.isRequired +}; + +export default AssignmentSync; diff --git a/src/components/Assignments/Assignment/AssignmentSync/index.js b/src/components/Assignments/Assignment/AssignmentSync/index.js new file mode 100644 index 000000000..505e8a8b2 --- /dev/null +++ b/src/components/Assignments/Assignment/AssignmentSync/index.js @@ -0,0 +1 @@ +export default from './AssignmentSync'; diff --git a/src/components/forms/EditSimpleLimitsForm/index.js b/src/components/forms/EditSimpleLimitsForm/index.js index ece1d0922..46652bfe3 100644 --- a/src/components/forms/EditSimpleLimitsForm/index.js +++ b/src/components/forms/EditSimpleLimitsForm/index.js @@ -1 +1 @@ -export default from './EditSimpleLimits'; +export default from './EditSimpleLimitsForm'; diff --git a/src/pages/Assignment/Assignment.js b/src/pages/Assignment/Assignment.js index c7280c19b..b2d0d2120 100644 --- a/src/pages/Assignment/Assignment.js +++ b/src/pages/Assignment/Assignment.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import { Col, Row, Alert } from 'react-bootstrap'; +import { Col, Row } from 'react-bootstrap'; import Button from '../../components/widgets/FlatButton'; import { LinkContainer } from 'react-router-bootstrap'; @@ -47,6 +47,7 @@ import LocalizedTexts from '../../components/helpers/LocalizedTexts'; import SubmitSolutionButton from '../../components/Assignments/SubmitSolutionButton'; import SubmitSolutionContainer from '../../containers/SubmitSolutionContainer'; import SubmissionsTable from '../../components/Assignments/SubmissionsTable'; +import AssignmentSync from '../../components/Assignments/Assignment/AssignmentSync'; import withLinks from '../../hoc/withLinks'; @@ -177,102 +178,10 @@ class Assignment extends Component { {(isSuperAdmin || isSupervisorOf(assignment.groupId)) && - (!assignment.exerciseSynchronizationInfo.exerciseConfig - .upToDate || - !assignment.exerciseSynchronizationInfo - .exerciseEnvironmentConfigs.upToDate || - !assignment.exerciseSynchronizationInfo.hardwareGroups - .upToDate || - !assignment.exerciseSynchronizationInfo.localizedTexts - .upToDate || - !assignment.exerciseSynchronizationInfo.limits.upToDate || - !assignment.exerciseSynchronizationInfo.scoreConfig.upToDate || - !assignment.exerciseSynchronizationInfo.scoreCalculator - .upToDate) && - -
- -

- -

-
- -
    - {!assignment.exerciseSynchronizationInfo.exerciseConfig - .upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo - .exerciseEnvironmentConfigs.upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo.hardwareGroups - .upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo.localizedTexts - .upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo.limits - .upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo.scoreConfig - .upToDate && -
  • - -
  • } - {!assignment.exerciseSynchronizationInfo.scoreCalculator - .upToDate && -
  • - -
  • } -
-
-

- -

-
- - } + } diff --git a/src/pages/EditExerciseConfig/EditExerciseConfig.js b/src/pages/EditExerciseConfig/EditExerciseConfig.js index 2e09ee84e..71b2f3a4d 100644 --- a/src/pages/EditExerciseConfig/EditExerciseConfig.js +++ b/src/pages/EditExerciseConfig/EditExerciseConfig.js @@ -90,9 +90,9 @@ class EditExerciseConfig extends Component { exerciseConfig, exerciseEnvironmentConfig, exerciseScoreConfig, - editEnvironmentSimpleLimits, + // editEnvironmentSimpleLimits, pipelines, - limits, + // limits, editScoreConfig, superadmin, intl: { locale } diff --git a/test/redux/simpleLimits.js b/test/redux/simpleLimits.js deleted file mode 100644 index a6b46752f..000000000 --- a/test/redux/simpleLimits.js +++ /dev/null @@ -1,149 +0,0 @@ -import { expect } from 'chai'; -import { createStore, combineReducers } from 'redux'; -import { reducer } from 'redux-form'; -import { - setHorizontally, - setVertically, - setAll -} from '../../src/redux/modules/simpleLimits'; - -const random = () => Math.random().toString().substring(2); - -const formA = random(); -const formB = random(); -const exerciseId = random(); -const runtimeEnvironmentId = random(); -const testA = random(); -const testB = random(); -const registeredFields = { - [`limits.${testA}.memory`]: { - name: `limits.${testA}.memory`, - type: 'Field', - count: 1 - }, - [`limits.${testA}.wall-time`]: { - name: `limits.${testA}.wall-time`, - type: 'Field', - count: 1 - }, - [`limits.${testB}.memory`]: { - name: `limits.${testB}.memory`, - type: 'Field', - count: 1 - }, - [`limits.${testB}.wall-time`]: { - name: `limits.${testB}.wall-time`, - type: 'Field', - count: 1 - } -}; - -const initialState = { - form: { - [formA]: { - registeredFields, - values: { - limits: { - [testA]: { memory: 1, 'wall-time': 2 }, - [testB]: { memory: 4, 'wall-time': 5 } - } - } - }, - [formB]: { - registeredFields, - values: { - limits: { - [testA]: { memory: 7, 'wall-time': 8 }, - [testB]: { memory: 10, 'wall-time': 11 } - } - } - } - } -}; - -const getTestStore = () => - createStore(combineReducers({ form: reducer }), initialState); - -describe('simpleLimits', () => { - describe('reducer', () => { - it('must copy values horizontally across the same tests', () => { - const { dispatch, getState } = getTestStore(); - - setHorizontally(formA, exerciseId, runtimeEnvironmentId, testA)( - dispatch, - getState - ); - - expect(getState()).to.eql({ - form: { - [formA]: initialState.form[formA], - [formB]: { - registeredFields, - values: { - limits: { - [testA]: initialState.form[formA].values.limits[testA], - [testB]: initialState.form[formB].values.limits[testB] - } - } - } - } - }); - }); - - it('must copy values vertically across the runtime environments', () => { - const { dispatch, getState } = getTestStore(); - - setVertically(formA, exerciseId, runtimeEnvironmentId, testA)( - dispatch, - getState - ); - - expect(getState()).to.eql({ - form: { - [formA]: { - registeredFields, - values: { - limits: { - [testA]: initialState.form[formA].values.limits[testA], - [testB]: initialState.form[formA].values.limits[testA] - } - } - }, - [formB]: initialState.form[formB] - } - }); - }); - - it('must copy values in all directions across the runtime environments', () => { - const { dispatch, getState } = getTestStore(); - - setAll(formA, exerciseId, runtimeEnvironmentId, testA)( - dispatch, - getState - ); - - expect(getState()).to.eql({ - form: { - [formA]: { - registeredFields, - values: { - limits: { - [testA]: initialState.form[formA].values.limits[testA], - [testB]: initialState.form[formA].values.limits[testA] - } - } - }, - [formB]: { - registeredFields, - values: { - limits: { - [testA]: initialState.form[formA].values.limits[testA], - [testB]: initialState.form[formA].values.limits[testA] - } - } - } - } - }); - }); - }); -}); From 18a228f01e8c51f31514a2eaf5e27333e3804337 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 7 Dec 2017 15:54:08 +0100 Subject: [PATCH 26/47] Exercise config reflects changes in test count --- .../EditExerciseSimpleConfigForm.js | 164 ++++---- .../EditExerciseSimpleConfigTest.js | 395 +++++++++--------- src/helpers/exerciseSimpleForm.js | 7 +- src/locales/cs.json | 121 +++++- src/locales/en.json | 121 +++++- .../EditExerciseSimpleConfig.js | 69 ++- 6 files changed, 538 insertions(+), 339 deletions(-) diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 86dab1731..0038ee8c0 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -1,98 +1,108 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { reduxForm, FieldArray, getFormValues } from 'redux-form'; +import { reduxForm, getFormValues } from 'redux-form'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { Alert } from 'react-bootstrap'; +import FormBox from '../../widgets/FormBox'; import EditExerciseSimpleConfigTest from './EditExerciseSimpleConfigTest'; import SubmitButton from '../SubmitButton'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; -class EditExerciseSimpleConfigForm extends Component { - render() { - const { - anyTouched, - submitting, - handleSubmit, - hasFailed = false, - hasSucceeded = false, - invalid, - formValues, - supplementaryFiles, - exerciseTests - } = this.props; - return ( -
- {hasFailed && - - - } - - {(...files) => - + + } + unlimitedHeight + success={submitSucceeded} + dirty={dirty} + footer={ +
+ + ), + submitting: ( + + ), + success: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + {(...files) => +
+ {exerciseTests.map((test, i) => + } - - -

- - ), - submitting: ( - - ), - success: ( - - ) - }} - /> -

-
- ); - } -} + testName={test.name} + test={`config.${i}`} + i={i} + /> + )} +
} + + ; EditExerciseSimpleConfigForm.propTypes = { initialValues: PropTypes.object, - values: PropTypes.array, handleSubmit: PropTypes.func.isRequired, anyTouched: PropTypes.bool, submitting: PropTypes.bool, hasFailed: PropTypes.bool, hasSucceeded: PropTypes.bool, + dirty: PropTypes.bool, + submitFailed: PropTypes.bool, + submitSucceeded: PropTypes.bool, invalid: PropTypes.bool, - exercise: PropTypes.shape({ - id: PropTypes.string.isRequired - }), formValues: PropTypes.object, supplementaryFiles: ImmutablePropTypes.map, exerciseTests: PropTypes.array @@ -186,6 +196,14 @@ export default connect((state, { exercise }) => { })( reduxForm({ form: 'editExerciseSimpleConfig', + enableReinitialize: true, + keepDirtyOnReinitialize: true, + immutableProps: [ + 'formValues', + 'supplementaryFiles', + 'exerciseTests', + 'handleSubmit' + ], validate })(EditExerciseSimpleConfigForm) ); diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index 04dc3e717..555341da0 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -52,11 +52,11 @@ const messages = defineMessages({ }); const EditExerciseSimpleConfigTest = ({ - fields, - prefix, supplementaryFiles, formValues, - exerciseTests, + testName, + test, + i, intl }) => { const supplementaryFilesOptions = [{ key: '', name: '...' }].concat( @@ -70,224 +70,221 @@ const EditExerciseSimpleConfigTest = ({ ); return (
- {fields.map((test, i) => -
- -
-

- {exerciseTests[i].name} -

- - - - -

- -

- - } - rightLabel={ - - } + + +

+ {testName} +

+ + + + +

+ +

+ - - } + } + rightLabel={ + - - -

- -

- - } + } + /> + + } + /> + + +

+ +

+ - - -

+ } + /> + +

+

+ +

+ + } + /> + {formValues && + formValues.config && + formValues.config[i] && + (formValues.config[i].useOutFile === true || + formValues.config[i].useOutFile === 'true') && + - - - } + } + />} + - {formValues && - formValues.config && - formValues.config[i] && - (formValues.config[i].useOutFile === true || - formValues.config[i].useOutFile === 'true') && - - } - />} - + + +

+ +

+ + } + /> + {formValues && + formValues.config && + formValues.config[i] && + (formValues.config[i].useCustomJudge === true || + formValues.config[i].useCustomJudge === 'true') + ? } /> - - -

- -

- } - /> - {formValues && - formValues.config && - formValues.config[i] && - (formValues.config[i].useCustomJudge === true || - formValues.config[i].useCustomJudge === 'true') - ? - } - /> - : - } - />} - {formValues && - formValues.config && - formValues.config[i] && - (formValues.config[i].useCustomJudge === true || - formValues.config[i].useCustomJudge === 'true') && - - } - />} - - - - )} + />} + {formValues && + formValues.config && + formValues.config[i] && + (formValues.config[i].useCustomJudge === true || + formValues.config[i].useCustomJudge === 'true') && + + } + />} + + ); }; EditExerciseSimpleConfigTest.propTypes = { - fields: PropTypes.object.isRequired, - prefix: PropTypes.string.isRequired, + testName: PropTypes.string.isRequired, + test: PropTypes.string.isRequired, + i: PropTypes.number.isRequired, supplementaryFiles: PropTypes.array.isRequired, exerciseTests: PropTypes.array, formValues: PropTypes.object, diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index d8480d9ef..496e7b2b0 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -170,10 +170,13 @@ export const transformAndSendConfigValues = ( formData, pipelines, environments, + sortedTests, setConfig ) => { let testVars = []; - for (const test of formData.config) { + for (let testIndex = 0; testIndex < sortedTests.length; ++testIndex) { + const test = formData.config[testIndex]; + const testName = sortedTests[testIndex].name; let variables = []; variables.push({ @@ -234,7 +237,7 @@ export const transformAndSendConfigValues = ( }); testVars.push({ - name: test.name, + name: testName, variables: variables, producesFiles: test.useOutFile }); diff --git a/src/locales/cs.json b/src/locales/cs.json index 5b89993ec..16ad8d1f4 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -53,9 +53,11 @@ "app.assignment.secondDeadline": "Druhý termín odevzdání:", "app.assignment.submissionsCountLimit": "Limit počtu odevzdaných řešení:", "app.assignment.syncButton": "Update this assignment", + "app.assignment.syncConfigType": "Type of exercise configuration", "app.assignment.syncDescription": "The exercise for this assignment was updated in following categories:", "app.assignment.syncExerciseConfig": "Exercise configuration", "app.assignment.syncExerciseEnvironmentConfigs": "Environment configuration", + "app.assignment.syncExerciseTests": "Exercise tests", "app.assignment.syncHardwareGroups": "Hardware groups", "app.assignment.syncLimits": "Limits", "app.assignment.syncLocalizedTexts": "Localized texts", @@ -132,6 +134,7 @@ "app.createGroup.isPublic": "Studenti se mohou sami přidávat k této skupině", "app.createGroup.publicStats": "Studenti mohou vidět dosažené body ostatních", "app.createGroup.threshold": "Minimální procentuální hranice potřebná ke splnění tohoto kurzu:", + "app.createGroup.validation.emptyDescription": "Group description cannot be empty.", "app.createGroup.validation.emptyName": "Název skupiny nemůže být prázdný.", "app.createGroup.validation.nameCollision": "Jméno \"{name}\" je již obsazené, prosíme vyberte jiné.", "app.createGroup.validation.thresholdBetweenZeroHundred": "Procentuální hranice musí být celé číslo od 0 do 100.", @@ -154,6 +157,7 @@ "app.editAssignment.deleteAssignmentWarning": "Smazání zadané úlohy odstraní všechna studentská řešní. Pro případné obnovení těchto dat prosím kontaktujte správce ReCodExu.", "app.editAssignment.description": "Změnit nastavení zadání úlohy včetně jejích limitů", "app.editAssignment.title": "Upravit zadání úlohy", + "app.editAssignmentForm.addLanguage": "Add language variant", "app.editAssignmentForm.allowSecondDeadline": "Povolit druhý termín odevzdání.", "app.editAssignmentForm.canViewLimitRatios": "Viditelnost poměrů dosažených výsledků vůči limitům", "app.editAssignmentForm.chooseFirstDeadlineBeforeSecondDeadline": "Před nastavením druhého termínu odevzdání je nutné zvolit první termín.", @@ -165,9 +169,16 @@ "app.editAssignmentForm.localized.assignment": "Zadání úlohy a popis pro studenty:", "app.editAssignmentForm.localized.locale": "Jazyková mutace:", "app.editAssignmentForm.localized.name": "Name:", + "app.editAssignmentForm.localized.noLanguage": "There is currently no text in any language for this assignment.", + "app.editAssignmentForm.localized.reallyRemoveQuestion": "Do you really want to delete the assignmenet in this language?", + "app.editAssignmentForm.localized.remove": "Remove this language", "app.editAssignmentForm.maxPointsBeforeFirstDeadline": "Maximální počet bodů pro řešení odevzdaná před prvním termínem:", "app.editAssignmentForm.maxPointsBeforeSecondDeadline": "Maximální počet bodů pro řešení odevzdaná před druhým termínem:", + "app.editAssignmentForm.moreAboutScoreConfig": "Read more about score configuration syntax.", + "app.editAssignmentForm.name": "Assignment default name:", + "app.editAssignmentForm.newLocale": "New language", "app.editAssignmentForm.pointsPercentualThreshold": "Minimální procentuální správnost řešení, za kterou lze získat nějaké body:", + "app.editAssignmentForm.scoreConfig": "Score configuration:", "app.editAssignmentForm.secondDeadline": "Druhý termín odevzdání:", "app.editAssignmentForm.submissionsCountLimit": "Počet pokusů odevzdání:", "app.editAssignmentForm.submit": "Upravit nastavení", @@ -188,6 +199,16 @@ "app.editAssignmentForm.validation.secondDeadline": "Prosíme vyplňte datum a čas druhého termínu odevzdání.", "app.editAssignmentForm.validation.secondDeadlineBeforeFirstDeadline": "Prosíme vyplňte datum a čas druhého termínu odevzdání, které je po {firstDeadline, date} {firstDeadline, time, short}.", "app.editAssignmentForm.validation.submissionsCountLimit": "Prosíme vyplňte maximální počet odevzdaných řešení jako kladné celé číslo.", + "app.editAssignmentLimitsForm.failed": "Saving failed. Please try again later.", + "app.editAssignmentLimitsForm.submit": "Change limits", + "app.editAssignmentLimitsForm.submitting": "Saving limits ...", + "app.editAssignmentLimitsForm.success": "Limits were saved.", + "app.editAssignmentLimitsForm.validation.envName": "Please fill environment name.", + "app.editAssignmentLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editAssignmentLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editAssignmentLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editAssignmentLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editAssignmentLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", "app.editEnvironmentConfigForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", "app.editEnvironmentConfigForm.submit": "Změnit konfiguraci", "app.editEnvironmentConfigForm.submitting": "Ukládání konfigurace ...", @@ -214,6 +235,13 @@ "app.editEnvironmentConfigVariables.stringArrayType": "Array of strings", "app.editEnvironmentConfigVariables.stringType": "Řetězec", "app.editEnvironmentConfigVariables.variables": "Proměnné:", + "app.editEnvironmentLimitsForm.box": "Box", + "app.editEnvironmentLimitsForm.environment.name": "Environment name:", + "app.editEnvironmentLimitsForm.environment.noEnvironment": "There is currently no environment specified for this assignment.", + "app.editEnvironmentLimitsForm.newEnvironment": "New environment", + "app.editEnvironmentLimitsForm.noBoxesForPipeline": "There are no boxes which need to set limits in this pipeline.", + "app.editEnvironmentLimitsForm.noPipelinesForTest": "There are no pipelines for this test to edit.", + "app.editEnvironmentLimitsForm.pipeline": "Pipeline", "app.editEnvironmentLimitsForm.submit": "Save changes to {env}", "app.editEnvironmentLimitsForm.submitting": "Saving changes ...", "app.editEnvironmentLimitsForm.success": "Saved.", @@ -247,14 +275,17 @@ "app.editExerciseConfigEnvironment.reallyRemoveQuestion": "Opravdu chcete smazat tuto konfiguraci prostředí?", "app.editExerciseConfigForm.addTest": "Přidat nový test", "app.editExerciseConfigForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", + "app.editExerciseConfigForm.fileType": "File", "app.editExerciseConfigForm.pipelines": "Pipeliny", "app.editExerciseConfigForm.removeLastTest": "Odstranit poslední test", + "app.editExerciseConfigForm.stringType": "String", "app.editExerciseConfigForm.submit": "Změnit konfiguraci", "app.editExerciseConfigForm.submitting": "Ukládání konfigurace ...", "app.editExerciseConfigForm.success": "Konfigurace byla uložena.", "app.editExerciseConfigForm.validation.duplicatePipeline": "Please select a different pipeline.", "app.editExerciseConfigForm.validation.noEnvironments": "Please add at least one environment config for the exercise.", "app.editExerciseConfigForm.variables": "Proměnné", + "app.editExerciseForm.description": "Description for supervisors:", "app.editExerciseForm.difficulty": "Obtížnost", "app.editExerciseForm.easy": "Snadné", "app.editExerciseForm.failed": "Uložení se nezdařilo. Prosim, opakujte akci později.", @@ -263,6 +294,7 @@ "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", "app.editExerciseForm.isPublic": "Úloha je veřejná a může být použita cvičícími.", "app.editExerciseForm.medium": "Průměrné", + "app.editExerciseForm.name": "Exercise name:", "app.editExerciseForm.submit": "Upravit nastavení", "app.editExerciseForm.submitting": "Ukládání změn ...", "app.editExerciseForm.success": "Nastavení bylo uloženo.", @@ -277,6 +309,28 @@ "app.editExerciseForm.validation.noLocalizedText": "Prosíme přidejte alespoň jeden lokalizovaný text popisující tuto úlohu.", "app.editExerciseForm.validation.sameLocalizedTexts": "Je vyplněno více jazykových variant pro jednu lokalizaci. Prosím ujistěte se, že lokalizace jsou unikátní.", "app.editExerciseForm.validation.versionDiffers": "Někdo změnil tuto úlohu v průběhu její editace. Prosíme obnovte si tuto stránku a proveďte své změny znovu.", + "app.editExerciseLimitsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseLimitsForm.submit": "Change limits", + "app.editExerciseLimitsForm.submitting": "Saving limits ...", + "app.editExerciseLimitsForm.success": "Limits were saved.", + "app.editExerciseLimitsForm.validation.envName": "Please fill environment name.", + "app.editExerciseLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editExerciseLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editExerciseLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editExerciseLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editExerciseLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", + "app.editExerciseRuntimeConfigsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseRuntimeConfigsForm.submit": "Change runtime configurations", + "app.editExerciseRuntimeConfigsForm.submitting": "Saving runtime configurations ...", + "app.editExerciseRuntimeConfigsForm.success": "Runtime configurations were saved.", + "app.editExerciseRuntimeConfigsForm.validation.empty": "Please fill the runtime environment information.", + "app.editExerciseRuntimeConfigsForm.validation.jobConfig": "Please fill the job configuration of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.name": "Please fill the display name of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.runtimeEnvironmentId": "Please select a runtime environment.", + "app.editExerciseSimpleConfigEnvironment.addConfigTab": "Add new runtime configuration", + "app.editExerciseSimpleConfigEnvironment.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editExerciseSimpleConfigEnvironment.newEnvironment": "New environment", + "app.editExerciseSimpleConfigEnvironment.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", "app.editExerciseSimpleConfigForm.submit": "Change configuration", "app.editExerciseSimpleConfigForm.submitting": "Saving configuration ...", "app.editExerciseSimpleConfigForm.success": "Configuration was changed.", @@ -298,6 +352,7 @@ "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", "app.editExerciseSimpleConfigTests.outputFile": "Output file:", "app.editExerciseSimpleConfigTests.outputTitle": "Output", + "app.editExerciseSimpleConfigTests.runtimeEnvironment": "Runtime environment:", "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", "app.editExerciseSimpleConfigTests.useOutfile": "Use output file instead of stdout", "app.editGroup.cannotDeleteRootGroup": "Toto je primární skupina a jako taková nemůže být smazána.", @@ -313,6 +368,7 @@ "app.editGroupForm.set": "Upravit skupinu", "app.editGroupForm.success": "Nastavení skupiny bylo uloženo.", "app.editGroupForm.successNew": "Create group", + "app.editGroupForm.title": "Edit group", "app.editGroupForm.titleEdit": "Edit group", "app.editGroupForm.titleNew": "Create new group", "app.editGroupForm.validation.emptyName": "Please fill the name of the group.", @@ -355,6 +411,14 @@ "app.editPipelineForm.validation.description": "Prosíme vyplňte popis této pipeliny.", "app.editPipelineForm.validation.emptyName": "Prosíme vyplňte název této pipeliny.", "app.editPipelineForm.validation.versionDiffers": "Někdo jiný změnit nastavení této pipeliny v průběhu vaší editace. Prosíme znovu načtěte tuto stránku a aplikujte své změny znovu.", + "app.editRuntimeConfigForm.addConfigTab": "Add new runtime configuration", + "app.editRuntimeConfigForm.configName": "Name of Configuration:", + "app.editRuntimeConfigForm.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editRuntimeConfigForm.jobConfig": "Job Configuration:", + "app.editRuntimeConfigForm.moreAboutJobConfig": "Read more about job configuration syntax.", + "app.editRuntimeConfigForm.newConfig": "New configuration", + "app.editRuntimeConfigForm.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", + "app.editRuntimeConfigForm.runtimeEnvironment": "Select runtime environment:", "app.editScoreConfigForm.failed": "Saving failed. Please try again later.", "app.editScoreConfigForm.scoreConfig": "Score configuration:", "app.editScoreConfigForm.submit": "Change configuration", @@ -391,6 +455,8 @@ "app.editUserProfile.validation.emptyFirstName": "Jméno nemůže být prázdné.", "app.editUserProfile.validation.emptyLastName": "Příjmení nemůže být prázdné.", "app.editUserProfile.validation.emptyNewPassword": "Nové heslo nemůže být prázdné pokud si měníte heslo.", + "app.editUserProfile.validation.emptyOldPassword": "Old password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.editUserProfile.validation.passwordsDontMatch": "Hesla se neshodují.", "app.editUserProfile.validation.samePasswords": "Změnit Vaše heslo na stejné nedává žádný smysl.", "app.editUserProfile.validation.shortFirstName": "First name must contain at least 2 characters.", @@ -477,6 +543,16 @@ "app.exercise.referenceSolutionsBox": "Referenční řešení", "app.exercise.runtimes": "Podporovaná běhová prostředí:", "app.exercise.updatedAt": "Naposledy změněno:", + "app.exercise.uploadReferenceSolution.createButton": "Create account", + "app.exercise.uploadReferenceSolution.createSuccessful": "Solution created successfully.", + "app.exercise.uploadReferenceSolution.creatingButtonText": "Creating solution ...", + "app.exercise.uploadReferenceSolution.creationFailed": "Solution was rejected by the server.", + "app.exercise.uploadReferenceSolution.instructions": "You must attach at least one file with source code and wait, until all your files are uploaded to the server. If there is a problem uploading any of the files, please try uploading it again or remove the file. This form cannot be submitted until there are any files which have not been successfully uploaded or which could not have been uploaded to the server.", + "app.exercise.uploadReferenceSolution.noteLabel": "Description of the provided exercise solution", + "app.exercise.uploadReferenceSolution.resetFormButton": "Reset form", + "app.exercise.uploadReferenceSolution.runtimeConfigs": "Runtime Configuration:", + "app.exercise.uploadReferenceSolution.validation.emptyNote": "Description of the solution cannot be empty.", + "app.exercise.uploadReferenceSolution.validation.runtimeId": "Please select one of the runtime configurations.", "app.exercise.uploadReferenceSolutionBox": "Vytvořit referenční řešení", "app.exercise.version": "Verze:", "app.exercises.add": "Přidat úlohu", @@ -566,10 +642,6 @@ "app.forkExerciseButton.fork": "Zduplikovat úlohu", "app.forkExerciseButton.loading": "Duplikování ...", "app.forkExerciseButton.success": "Ukázat zduplikovanou úlohu", - "app.forkExerciseForm.failed": "Saving failed. Please try again later.", - "app.forkExerciseForm.submit": "Fork exercise", - "app.forkExerciseForm.submitting": "Forking ...", - "app.forkExerciseForm.success": "Exercise forked", "app.forkPipelineButton.success": "Show the forked pipeline", "app.forkPipelineForm.failed": "Saving failed. Please try again later.", "app.forkPipelineForm.submit": "Fork pipeline", @@ -607,6 +679,10 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Odebrat cvičícího", "app.groupsName.loading": "Načítání ...", + "app.hardwareGroupFields.memoryLimit": "Memory limit for \"{taskId}\":", + "app.hardwareGroupFields.noReferenceSolutions": "There are no reference solutions' evaluations' for test '{testName}' and its task '{taskId}'.", + "app.hardwareGroupFields.test": "Test:", + "app.hardwareGroupFields.timeLimit": "Time limit for \"{taskId}\":", "app.header.toggleSidebar": "Zobrazit/skrýt boční panel", "app.header.toggleSidebarSize": "Zvětšit/zmenšit boční panel", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -651,6 +727,15 @@ "app.login.description": "Prosíme zadejte své přihlašovací údaje", "app.login.resetPassword": "Obnovte si své heslo.", "app.login.title": "Přihlásit se", + "app.loginCASForm.failed": "Login failed. Please check your credentials.", + "app.loginCASForm.login": "Sign in", + "app.loginCASForm.password": "Password:", + "app.loginCASForm.processing": "Signing in ...", + "app.loginCASForm.success": "You are successfully signed in", + "app.loginCASForm.title": "Sign into ReCodEx using CAS UK", + "app.loginCASForm.ukco": "UKCO (student's number):", + "app.loginCASForm.validation.emptyPassword": "Password cannot be empty.", + "app.loginCASForm.validation.emptyUKCO": "UKCO address cannot be empty.", "app.loginForm.email": "E-mailová adresa:", "app.loginForm.failed": "Přihlášení selhalo. Prosíme zkontrolujte své přihlašovací údaje.", "app.loginForm.login": "Přihlásit se", @@ -794,6 +879,12 @@ "app.referenceSolutionDetail.title.details": "Detail referenčního řešení", "app.referenceSolutionDetail.uploadedAt": "Nahráno:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", + "app.referenceSolutionEvaluation.titlePrefix": "Evaluations for runtime:", + "app.referenceSolutionsEvaluations.description": "Description", + "app.referenceSolutionsEvaluations.evaluatedAt": "Evaluated on", + "app.referenceSolutionsEvaluations.memory": "Memory", + "app.referenceSolutionsEvaluations.time": "Time", + "app.referenceSolutionsEvaluations.title": "Reference solutions' evaluations", "app.registration.description": "Začněte dnes používat ReCodEx", "app.registration.title": "Vytvořte si nový účet v ReCodExu", "app.registrationForm.createAccount": "Vytvořit účet", @@ -814,6 +905,7 @@ "app.registrationForm.validation.emptyLastName": "Příjmení nemůže být prázdné.", "app.registrationForm.validation.emptyPassword": "Heslo nemůže být prázdné.", "app.registrationForm.validation.passwordDontMatch": "Passwords don't match.", + "app.registrationForm.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.registrationForm.validation.shortFirstName": "First name must contain at least 2 characters.", "app.registrationForm.validation.shortLastName": "Last name must contain at least 2 characters.", "app.removeFromGroup.confirm": "Are you sure you want to remove the user from this group?", @@ -952,7 +1044,10 @@ "app.sudebar.menu.supervisor.title": "Cvičící", "app.supplementaryFiles.deleteButton": "Delete", "app.supplementaryFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.supplementaryFilesTable.addFiles": "Save supplementary files", "app.supplementaryFilesTable.description": "Soubory úlohy jsou soubory, které mohou být použity v nastavení úlohy jako vstupy nebo vzorové výstupy.", + "app.supplementaryFilesTable.empty": "There are no supplementary files attached to this exercise yet.", + "app.supplementaryFilesTable.fileHashName": "Hash Name", "app.supplementaryFilesTable.fileName": "Původní název", "app.supplementaryFilesTable.fileSize": "Velikost souboru", "app.supplementaryFilesTable.fileUploadedAt": "Nahrán", @@ -983,13 +1078,13 @@ "app.usersName.notVerified.title": "Tento účet nemá ověřenou emailovou adresu.", "app.usersStats.description": "Body získané ve skupině {name}.", "app.usersname.notVerified.description": "Tento uživatel si neověřil svou emailovou adresu přes aktivační odkaz, který mu byl na tuto adresu zaslán.", - "diff": "Diff", - "recodex-judge-float": "Floating point judge", - "recodex-judge-float-newline": "Floating judge ignoring newlines", - "recodex-judge-normal": "Normal judge", - "recodex-judge-normal-newline": "Normal judge ignoring newlines", - "recodex-judge-shuffle": "Shuffle judge", - "recodex-judge-shuffle-all": "Shuffle judge ignoring all", - "recodex-judge-shuffle-newline": "Shuffle judge ignoring newlines", - "recodex-judge-shuffle-rows": "Shuffle judge ignoring rows" + "diff": "Binary-safe judge", + "recodex-judge-float": "Float-numbers judge", + "recodex-judge-float-newline": "Float-numbers judge (ignoring ends of lines)", + "recodex-judge-normal": "Token judge", + "recodex-judge-normal-newline": "Token judge (ignoring ends of lines)", + "recodex-judge-shuffle": "Unordered-tokens judge", + "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", + "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", + "recodex-judge-shuffle-rows": "Unordered-rows judge" } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index ca29ff78a..69e24f726 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -53,9 +53,11 @@ "app.assignment.secondDeadline": "Second deadline:", "app.assignment.submissionsCountLimit": "Submission count limit:", "app.assignment.syncButton": "Update this assignment", + "app.assignment.syncConfigType": "Type of exercise configuration", "app.assignment.syncDescription": "The exercise for this assignment was updated in following categories:", "app.assignment.syncExerciseConfig": "Exercise configuration", "app.assignment.syncExerciseEnvironmentConfigs": "Environment configuration", + "app.assignment.syncExerciseTests": "Exercise tests", "app.assignment.syncHardwareGroups": "Hardware groups", "app.assignment.syncLimits": "Limits", "app.assignment.syncLocalizedTexts": "Localized texts", @@ -132,6 +134,7 @@ "app.createGroup.isPublic": "Students can join the group themselves", "app.createGroup.publicStats": "Students can see statistics of each other", "app.createGroup.threshold": "Minimum percent of the total points count needed to complete the course:", + "app.createGroup.validation.emptyDescription": "Group description cannot be empty.", "app.createGroup.validation.emptyName": "Group name cannot be empty.", "app.createGroup.validation.nameCollision": "The name \"{name}\" is already used, please choose a different one.", "app.createGroup.validation.thresholdBetweenZeroHundred": "Threshold must be an integer in between 0 and 100.", @@ -154,6 +157,7 @@ "app.editAssignment.deleteAssignmentWarning": "Deleting an assignment will remove all the students submissions and you will have to contact the administrator of ReCodEx if you wanted to restore the assignment in the future.", "app.editAssignment.description": "Change assignment settings and limits", "app.editAssignment.title": "Edit assignment settings", + "app.editAssignmentForm.addLanguage": "Add language variant", "app.editAssignmentForm.allowSecondDeadline": "Allow second deadline.", "app.editAssignmentForm.canViewLimitRatios": "Visibility of memory and time ratios", "app.editAssignmentForm.chooseFirstDeadlineBeforeSecondDeadline": "You must select the date of the first deadline before selecting the date of the second deadline.", @@ -165,9 +169,16 @@ "app.editAssignmentForm.localized.assignment": "Description for the students:", "app.editAssignmentForm.localized.locale": "The language:", "app.editAssignmentForm.localized.name": "Name:", + "app.editAssignmentForm.localized.noLanguage": "There is currently no text in any language for this assignment.", + "app.editAssignmentForm.localized.reallyRemoveQuestion": "Do you really want to delete the assignmenet in this language?", + "app.editAssignmentForm.localized.remove": "Remove this language", "app.editAssignmentForm.maxPointsBeforeFirstDeadline": "Maximum amount of points received when submitted before the deadline:", "app.editAssignmentForm.maxPointsBeforeSecondDeadline": "Maximum amount of points received when submitted before the second deadline:", + "app.editAssignmentForm.moreAboutScoreConfig": "Read more about score configuration syntax.", + "app.editAssignmentForm.name": "Assignment default name:", + "app.editAssignmentForm.newLocale": "New language", "app.editAssignmentForm.pointsPercentualThreshold": "Minimum percentage of points which submissions have to gain:", + "app.editAssignmentForm.scoreConfig": "Score configuration:", "app.editAssignmentForm.secondDeadline": "Second deadline:", "app.editAssignmentForm.submissionsCountLimit": "Submissions count limit:", "app.editAssignmentForm.submit": "Edit settings", @@ -188,6 +199,16 @@ "app.editAssignmentForm.validation.secondDeadline": "Please fill the date and time of the second deadline.", "app.editAssignmentForm.validation.secondDeadlineBeforeFirstDeadline": "Please fill the date and time of the second deadline with a value which is after {firstDeadline, date} {firstDeadline, time, short}.", "app.editAssignmentForm.validation.submissionsCountLimit": "Please fill the submissions count limit field with a positive integer.", + "app.editAssignmentLimitsForm.failed": "Saving failed. Please try again later.", + "app.editAssignmentLimitsForm.submit": "Change limits", + "app.editAssignmentLimitsForm.submitting": "Saving limits ...", + "app.editAssignmentLimitsForm.success": "Limits were saved.", + "app.editAssignmentLimitsForm.validation.envName": "Please fill environment name.", + "app.editAssignmentLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editAssignmentLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editAssignmentLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editAssignmentLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editAssignmentLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", "app.editEnvironmentConfigForm.failed": "Saving failed. Please try again later.", "app.editEnvironmentConfigForm.submit": "Change configuration", "app.editEnvironmentConfigForm.submitting": "Saving configuration ...", @@ -214,6 +235,13 @@ "app.editEnvironmentConfigVariables.stringArrayType": "Array of strings", "app.editEnvironmentConfigVariables.stringType": "String", "app.editEnvironmentConfigVariables.variables": "Variables:", + "app.editEnvironmentLimitsForm.box": "Box", + "app.editEnvironmentLimitsForm.environment.name": "Environment name:", + "app.editEnvironmentLimitsForm.environment.noEnvironment": "There is currently no environment specified for this assignment.", + "app.editEnvironmentLimitsForm.newEnvironment": "New environment", + "app.editEnvironmentLimitsForm.noBoxesForPipeline": "There are no boxes which need to set limits in this pipeline.", + "app.editEnvironmentLimitsForm.noPipelinesForTest": "There are no pipelines for this test to edit.", + "app.editEnvironmentLimitsForm.pipeline": "Pipeline", "app.editEnvironmentLimitsForm.submit": "Save changes to {env}", "app.editEnvironmentLimitsForm.submitting": "Saving changes ...", "app.editEnvironmentLimitsForm.success": "Saved.", @@ -247,14 +275,17 @@ "app.editExerciseConfigEnvironment.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", "app.editExerciseConfigForm.addTest": "Add new test", "app.editExerciseConfigForm.failed": "Saving failed. Please try again later.", + "app.editExerciseConfigForm.fileType": "File", "app.editExerciseConfigForm.pipelines": "Pipelines", "app.editExerciseConfigForm.removeLastTest": "Remove last test", + "app.editExerciseConfigForm.stringType": "String", "app.editExerciseConfigForm.submit": "Change configuration", "app.editExerciseConfigForm.submitting": "Saving configuration ...", "app.editExerciseConfigForm.success": "Configuration was changed.", "app.editExerciseConfigForm.validation.duplicatePipeline": "Please select a different pipeline.", "app.editExerciseConfigForm.validation.noEnvironments": "Please add at least one environment config for the exercise.", "app.editExerciseConfigForm.variables": "Variables", + "app.editExerciseForm.description": "Description for supervisors:", "app.editExerciseForm.difficulty": "Difficulty", "app.editExerciseForm.easy": "Easy", "app.editExerciseForm.failed": "Saving failed. Please try again later.", @@ -263,6 +294,7 @@ "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", "app.editExerciseForm.isPublic": "Exercise is public and can be assigned to students by their supervisors.", "app.editExerciseForm.medium": "Medium", + "app.editExerciseForm.name": "Exercise name:", "app.editExerciseForm.submit": "Edit settings", "app.editExerciseForm.submitting": "Saving changes ...", "app.editExerciseForm.success": "Settings were saved.", @@ -277,6 +309,28 @@ "app.editExerciseForm.validation.noLocalizedText": "Please add at least one localized text describing the exercise.", "app.editExerciseForm.validation.sameLocalizedTexts": "There are more language variants with the same locale. Please make sure locales are unique.", "app.editExerciseForm.validation.versionDiffers": "Somebody has changed the exercise while you have been editing it. Please reload the page and apply your changes once more.", + "app.editExerciseLimitsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseLimitsForm.submit": "Change limits", + "app.editExerciseLimitsForm.submitting": "Saving limits ...", + "app.editExerciseLimitsForm.success": "Limits were saved.", + "app.editExerciseLimitsForm.validation.envName": "Please fill environment name.", + "app.editExerciseLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editExerciseLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editExerciseLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editExerciseLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editExerciseLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", + "app.editExerciseRuntimeConfigsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseRuntimeConfigsForm.submit": "Change runtime configurations", + "app.editExerciseRuntimeConfigsForm.submitting": "Saving runtime configurations ...", + "app.editExerciseRuntimeConfigsForm.success": "Runtime configurations were saved.", + "app.editExerciseRuntimeConfigsForm.validation.empty": "Please fill the runtime environment information.", + "app.editExerciseRuntimeConfigsForm.validation.jobConfig": "Please fill the job configuration of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.name": "Please fill the display name of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.runtimeEnvironmentId": "Please select a runtime environment.", + "app.editExerciseSimpleConfigEnvironment.addConfigTab": "Add new runtime configuration", + "app.editExerciseSimpleConfigEnvironment.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editExerciseSimpleConfigEnvironment.newEnvironment": "New environment", + "app.editExerciseSimpleConfigEnvironment.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", "app.editExerciseSimpleConfigForm.submit": "Change configuration", "app.editExerciseSimpleConfigForm.submitting": "Saving configuration ...", "app.editExerciseSimpleConfigForm.success": "Configuration was changed.", @@ -298,6 +352,7 @@ "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", "app.editExerciseSimpleConfigTests.outputFile": "Output file:", "app.editExerciseSimpleConfigTests.outputTitle": "Output", + "app.editExerciseSimpleConfigTests.runtimeEnvironment": "Runtime environment:", "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", "app.editExerciseSimpleConfigTests.useOutfile": "Use output file instead of stdout", "app.editGroup.cannotDeleteRootGroup": "This is a so-called root group and it cannot be deleted.", @@ -313,6 +368,7 @@ "app.editGroupForm.set": "Edit group", "app.editGroupForm.success": "Group settings was saved.", "app.editGroupForm.successNew": "Create group", + "app.editGroupForm.title": "Edit group", "app.editGroupForm.titleEdit": "Edit group", "app.editGroupForm.titleNew": "Create new group", "app.editGroupForm.validation.emptyName": "Please fill the name of the group.", @@ -355,6 +411,14 @@ "app.editPipelineForm.validation.description": "Please fill the description of the pipeline.", "app.editPipelineForm.validation.emptyName": "Please fill the name of the pipeline.", "app.editPipelineForm.validation.versionDiffers": "Somebody has changed the pipeline while you have been editing it. Please reload the page and apply your changes once more.", + "app.editRuntimeConfigForm.addConfigTab": "Add new runtime configuration", + "app.editRuntimeConfigForm.configName": "Name of Configuration:", + "app.editRuntimeConfigForm.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editRuntimeConfigForm.jobConfig": "Job Configuration:", + "app.editRuntimeConfigForm.moreAboutJobConfig": "Read more about job configuration syntax.", + "app.editRuntimeConfigForm.newConfig": "New configuration", + "app.editRuntimeConfigForm.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", + "app.editRuntimeConfigForm.runtimeEnvironment": "Select runtime environment:", "app.editScoreConfigForm.failed": "Saving failed. Please try again later.", "app.editScoreConfigForm.scoreConfig": "Score configuration:", "app.editScoreConfigForm.submit": "Change configuration", @@ -391,6 +455,8 @@ "app.editUserProfile.validation.emptyFirstName": "First name cannot be empty.", "app.editUserProfile.validation.emptyLastName": "Last name cannot be empty.", "app.editUserProfile.validation.emptyNewPassword": "New password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.emptyOldPassword": "Old password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.editUserProfile.validation.passwordsDontMatch": "Passwords don't match.", "app.editUserProfile.validation.samePasswords": "Changing your password to the same password does not make any sense.", "app.editUserProfile.validation.shortFirstName": "First name must contain at least 2 characters.", @@ -477,6 +543,16 @@ "app.exercise.referenceSolutionsBox": "Reference solutions", "app.exercise.runtimes": "Supported runtime environments:", "app.exercise.updatedAt": "Last updated at:", + "app.exercise.uploadReferenceSolution.createButton": "Create account", + "app.exercise.uploadReferenceSolution.createSuccessful": "Solution created successfully.", + "app.exercise.uploadReferenceSolution.creatingButtonText": "Creating solution ...", + "app.exercise.uploadReferenceSolution.creationFailed": "Solution was rejected by the server.", + "app.exercise.uploadReferenceSolution.instructions": "You must attach at least one file with source code and wait, until all your files are uploaded to the server. If there is a problem uploading any of the files, please try uploading it again or remove the file. This form cannot be submitted until there are any files which have not been successfully uploaded or which could not have been uploaded to the server.", + "app.exercise.uploadReferenceSolution.noteLabel": "Description of the provided exercise solution", + "app.exercise.uploadReferenceSolution.resetFormButton": "Reset form", + "app.exercise.uploadReferenceSolution.runtimeConfigs": "Runtime Configuration:", + "app.exercise.uploadReferenceSolution.validation.emptyNote": "Description of the solution cannot be empty.", + "app.exercise.uploadReferenceSolution.validation.runtimeId": "Please select one of the runtime configurations.", "app.exercise.uploadReferenceSolutionBox": "Create reference solution", "app.exercise.version": "Version:", "app.exercises.add": "Add exercise", @@ -566,10 +642,6 @@ "app.forkExerciseButton.fork": "Fork the exercise", "app.forkExerciseButton.loading": "Forking ...", "app.forkExerciseButton.success": "Show the forked exercise", - "app.forkExerciseForm.failed": "Saving failed. Please try again later.", - "app.forkExerciseForm.submit": "Fork exercise", - "app.forkExerciseForm.submitting": "Forking ...", - "app.forkExerciseForm.success": "Exercise forked", "app.forkPipelineButton.success": "Show the forked pipeline", "app.forkPipelineForm.failed": "Saving failed. Please try again later.", "app.forkPipelineForm.submit": "Fork pipeline", @@ -607,6 +679,10 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Remove supervisor", "app.groupsName.loading": "Loading ...", + "app.hardwareGroupFields.memoryLimit": "Memory limit for \"{taskId}\":", + "app.hardwareGroupFields.noReferenceSolutions": "There are no reference solutions' evaluations' for test '{testName}' and its task '{taskId}'.", + "app.hardwareGroupFields.test": "Test:", + "app.hardwareGroupFields.timeLimit": "Time limit for \"{taskId}\":", "app.header.toggleSidebar": "Show/hide sidebar", "app.header.toggleSidebarSize": "Expand/minimize sidebar", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -651,6 +727,15 @@ "app.login.description": "Please fill your credentials", "app.login.resetPassword": "Reset your password.", "app.login.title": "Sign in", + "app.loginCASForm.failed": "Login failed. Please check your credentials.", + "app.loginCASForm.login": "Sign in", + "app.loginCASForm.password": "Password:", + "app.loginCASForm.processing": "Signing in ...", + "app.loginCASForm.success": "You are successfully signed in", + "app.loginCASForm.title": "Sign into ReCodEx using CAS UK", + "app.loginCASForm.ukco": "UKCO (student's number):", + "app.loginCASForm.validation.emptyPassword": "Password cannot be empty.", + "app.loginCASForm.validation.emptyUKCO": "UKCO address cannot be empty.", "app.loginForm.email": "E-mail address:", "app.loginForm.failed": "Login failed. Please check your credentials.", "app.loginForm.login": "Sign in", @@ -794,6 +879,12 @@ "app.referenceSolutionDetail.title.details": "Reference solution detail", "app.referenceSolutionDetail.uploadedAt": "Uploaded at:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", + "app.referenceSolutionEvaluation.titlePrefix": "Evaluations for runtime:", + "app.referenceSolutionsEvaluations.description": "Description", + "app.referenceSolutionsEvaluations.evaluatedAt": "Evaluated on", + "app.referenceSolutionsEvaluations.memory": "Memory", + "app.referenceSolutionsEvaluations.time": "Time", + "app.referenceSolutionsEvaluations.title": "Reference solutions' evaluations", "app.registration.description": "Start using ReCodEx today", "app.registration.title": "Create a new ReCodEx account", "app.registrationForm.createAccount": "Create account", @@ -814,6 +905,7 @@ "app.registrationForm.validation.emptyLastName": "Last name cannot be empty.", "app.registrationForm.validation.emptyPassword": "Password cannot be empty.", "app.registrationForm.validation.passwordDontMatch": "Passwords don't match.", + "app.registrationForm.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.registrationForm.validation.shortFirstName": "First name must contain at least 2 characters.", "app.registrationForm.validation.shortLastName": "Last name must contain at least 2 characters.", "app.removeFromGroup.confirm": "Are you sure you want to remove the user from this group?", @@ -952,7 +1044,10 @@ "app.sudebar.menu.supervisor.title": "Supervisor", "app.supplementaryFiles.deleteButton": "Delete", "app.supplementaryFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.supplementaryFilesTable.addFiles": "Save supplementary files", "app.supplementaryFilesTable.description": "Supplementary files are files which can be used in job configuration.", + "app.supplementaryFilesTable.empty": "There are no supplementary files attached to this exercise yet.", + "app.supplementaryFilesTable.fileHashName": "Hash Name", "app.supplementaryFilesTable.fileName": "Original filename", "app.supplementaryFilesTable.fileSize": "Filesize", "app.supplementaryFilesTable.fileUploadedAt": "Uploaded at", @@ -983,13 +1078,13 @@ "app.usersName.notVerified.title": "This account does not have a verified email address yet.", "app.usersStats.description": "Points gained from {name}.", "app.usersname.notVerified.description": "This user has not verified his/her email address via an activation link he has received to his email address.", - "diff": "Diff", - "recodex-judge-float": "Floating point judge", - "recodex-judge-float-newline": "Floating judge ignoring newlines", - "recodex-judge-normal": "Normal judge", - "recodex-judge-normal-newline": "Normal judge ignoring newlines", - "recodex-judge-shuffle": "Shuffle judge", - "recodex-judge-shuffle-all": "Shuffle judge ignoring all", - "recodex-judge-shuffle-newline": "Shuffle judge ignoring newlines", - "recodex-judge-shuffle-rows": "Shuffle judge ignoring rows" + "diff": "Binary-safe judge", + "recodex-judge-float": "Float-numbers judge", + "recodex-judge-float-newline": "Float-numbers judge (ignoring ends of lines)", + "recodex-judge-normal": "Token judge", + "recodex-judge-normal-newline": "Token judge (ignoring ends of lines)", + "recodex-judge-shuffle": "Unordered-tokens judge", + "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", + "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", + "recodex-judge-shuffle-rows": "Unordered-rows judge" } \ No newline at end of file diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index 2d1a37b99..13a4864f7 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -225,48 +225,39 @@ class EditExerciseSimpleConfig extends Component { - - } - unlimitedHeight + - - {(config, tests, environments, ...pipelines) => { - const sortedTests = tests.sort((a, b) => - a.name.localeCompare(b.name, locale) - ); - return ( - { + const sortedTests = tests.sort((a, b) => + a.name.localeCompare(b.name, locale) + ); + return ( + + transformAndSendConfigValues( + data, + pipelines, + environments, sortedTests, - locale + setConfig )} - exercise={exercise} - exerciseTests={sortedTests} - onSubmit={data => - transformAndSendConfigValues( - data, - pipelines, - environments, - setConfig - )} - /> - ); - }} - - + /> + ); + }} +
From 967467bce8adf97b1ae9ed926660226604737ddd Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 7 Dec 2017 16:50:14 +0100 Subject: [PATCH 27/47] Better fields validation, visual improvements --- .../EditExerciseSimpleConfigForm.css | 7 +++++++ .../EditExerciseSimpleConfigForm.js | 1 + .../EditExerciseSimpleConfigTest.js | 4 +++- .../EditSimpleLimitsForm.js | 1 + .../forms/Fields/ExpandingInputFilesField.js | 11 +++-------- .../forms/Fields/ExpandingSelectField.js | 11 +++-------- .../forms/Fields/ExpandingTextField.js | 11 +++-------- src/components/forms/Fields/SelectField.js | 17 ++++++++--------- 8 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.css diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.css b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.css new file mode 100644 index 000000000..043e4145c --- /dev/null +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.css @@ -0,0 +1,7 @@ +.configRow { + padding: 15px; +} + +.configRow:nth-child(odd) { + background-color: #F9F9F9; +} diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 0038ee8c0..463619cde 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -32,6 +32,7 @@ const EditExerciseSimpleConfigForm = ({ /> } unlimitedHeight + noPadding success={submitSucceeded} dirty={dirty} footer={ diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index 555341da0..5dc56dd1b 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -12,6 +12,8 @@ import { CheckboxField } from '../Fields'; +import './EditExerciseSimpleConfigForm.css'; + const messages = defineMessages({ normal: { id: 'recodex-judge-normal', @@ -69,7 +71,7 @@ const EditExerciseSimpleConfigTest = ({ })) ); return ( -
+

diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index 312bd8eee..081bea7d5 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -38,6 +38,7 @@ const EditSimpleLimitsForm = ({ /> } unlimitedHeight + noPadding success={submitSucceeded} dirty={dirty} footer={ diff --git a/src/components/forms/Fields/ExpandingInputFilesField.js b/src/components/forms/Fields/ExpandingInputFilesField.js index 50e80442b..59ce70d43 100644 --- a/src/components/forms/Fields/ExpandingInputFilesField.js +++ b/src/components/forms/Fields/ExpandingInputFilesField.js @@ -61,7 +61,7 @@ class ExpandingInputFilesField extends Component { leftLabel = '', rightLabel = '', input: { onChange, ...input }, - meta: { touched, error }, + meta: { dirty, error }, style = {}, options, ...props @@ -71,7 +71,7 @@ class ExpandingInputFilesField extends Component { return (

@@ -119,12 +119,7 @@ class ExpandingInputFilesField extends Component { {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} } ); diff --git a/src/components/forms/Fields/ExpandingSelectField.js b/src/components/forms/Fields/ExpandingSelectField.js index cabf21e58..1ad08eda5 100644 --- a/src/components/forms/Fields/ExpandingSelectField.js +++ b/src/components/forms/Fields/ExpandingSelectField.js @@ -45,7 +45,7 @@ class ExpandingSelectField extends Component { const { label = '', input: { name, onChange }, - meta: { touched, error }, + meta: { dirty, error }, options, style = {}, ...props @@ -55,7 +55,7 @@ class ExpandingSelectField extends Component { return ( {label}
@@ -78,12 +78,7 @@ class ExpandingSelectField extends Component {
{' '} {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} }
); diff --git a/src/components/forms/Fields/ExpandingTextField.js b/src/components/forms/Fields/ExpandingTextField.js index df9728b0c..1d77f276a 100644 --- a/src/components/forms/Fields/ExpandingTextField.js +++ b/src/components/forms/Fields/ExpandingTextField.js @@ -49,7 +49,7 @@ class ExpandingTextField extends Component { const { label = '', input: { onChange }, - meta: { touched, error }, + meta: { dirty, error }, style = {}, ...props } = this.props; @@ -58,7 +58,7 @@ class ExpandingTextField extends Component { return ( {label}
@@ -75,12 +75,7 @@ class ExpandingTextField extends Component {
{' '} {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} }
); diff --git a/src/components/forms/Fields/SelectField.js b/src/components/forms/Fields/SelectField.js index 4b56e79a0..d75780a97 100644 --- a/src/components/forms/Fields/SelectField.js +++ b/src/components/forms/Fields/SelectField.js @@ -11,7 +11,7 @@ import { const SelectField = ({ input, - meta: { touched, error }, + meta: { touched, dirty, error }, label, options, addEmptyOption = false, @@ -20,7 +20,7 @@ const SelectField = ({ }) => {label} @@ -38,12 +38,7 @@ const SelectField = ({ {error && - {touched - ? error - : } + {' '}{error}{' '} } ; @@ -51,7 +46,11 @@ SelectField.propTypes = { input: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired, - meta: PropTypes.shape({ error: PropTypes.any, touched: PropTypes.bool }), + meta: PropTypes.shape({ + error: PropTypes.any, + dirty: PropTypes.bool, + touched: PropTypes.bool + }), type: PropTypes.string, label: PropTypes.oneOfType([ PropTypes.string, From f73a09bc84c45143b02b37d3b3e164deed1b46a6 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 7 Dec 2017 16:56:50 +0100 Subject: [PATCH 28/47] Final polishing of tests edit form. --- .../forms/EditTestsForm/EditTestsForm.js | 12 +- .../forms/EditTestsForm/EditTestsTest.js | 106 +++++++++++------- .../forms/EditTestsForm/EditTestsTestRow.js | 8 +- src/components/helpers/stringFormatters.js | 8 ++ src/helpers/exerciseSimpleForm.js | 2 +- src/locales/cs.json | 58 +++++----- src/locales/en.json | 38 +++---- 7 files changed, 131 insertions(+), 101 deletions(-) diff --git a/src/components/forms/EditTestsForm/EditTestsForm.js b/src/components/forms/EditTestsForm/EditTestsForm.js index 235c4fb1c..4e3c6c122 100644 --- a/src/components/forms/EditTestsForm/EditTestsForm.js +++ b/src/components/forms/EditTestsForm/EditTestsForm.js @@ -12,7 +12,7 @@ import SubmitButton from '../SubmitButton'; class EditTestsForm extends Component { render() { const { - anyTouched, + dirty, submitting, handleSubmit, hasFailed = false, @@ -55,26 +55,26 @@ class EditTestsForm extends Component { invalid={invalid} submitting={submitting} hasSucceeded={hasSucceeded} - dirty={anyTouched} + dirty={dirty} hasFailed={hasFailed} handleSubmit={handleSubmit} messages={{ submit: ( ), submitting: ( ), success: ( ) }} @@ -88,7 +88,7 @@ class EditTestsForm extends Component { EditTestsForm.propTypes = { values: PropTypes.array, handleSubmit: PropTypes.func.isRequired, - anyTouched: PropTypes.bool, + dirty: PropTypes.bool, submitting: PropTypes.bool, hasFailed: PropTypes.bool, hasSucceeded: PropTypes.bool, diff --git a/src/components/forms/EditTestsForm/EditTestsTest.js b/src/components/forms/EditTestsForm/EditTestsTest.js index 382488306..0db718119 100644 --- a/src/components/forms/EditTestsForm/EditTestsTest.js +++ b/src/components/forms/EditTestsForm/EditTestsTest.js @@ -1,56 +1,82 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Table, Button } from 'react-bootstrap'; +import { formValues } from 'redux-form'; import Icon from 'react-fontawesome'; import { FormattedMessage } from 'react-intl'; import EditTestsTestRow from './EditTestsTestRow'; +import { prettyPrintPercent } from '../../helpers/stringFormatters'; -const EditTestsTest = ({ fields, isUniform }) => -
-
- - } - {...props} + -
- - {Number.isNaN(Number(wallTimeValue)) - ? '-' - : prettyMs(Number(wallTimeValue) * 1000)} - -
- {testsCount > 1 && - + } {environmentsCount > 1 && 1 && ({ - memoryValue: `${props.prefix}.memory`, - wallTimeValue: `${props.prefix}.wall-time` -}))(EditSimpleLimitsField); +export default EditSimpleLimitsField; diff --git a/src/components/forms/Fields/EditSimpleLimitsField.less b/src/components/forms/Fields/EditSimpleLimitsField.less index 8669a6447..025ea7747 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.less +++ b/src/components/forms/Fields/EditSimpleLimitsField.less @@ -3,6 +3,6 @@ text-align: center; color: #999; padding-left: 10px; - line-height: 2em; + padding-bottom: 0; vertical-align: top; } diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 8f0307135..79a0ddd80 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -1,4 +1,9 @@ import yaml from 'js-yaml'; +import { + endpointDisguisedAsIdFactory, + encodeTestName, + encodeEnvironmentId +} from '../redux/modules/simpleLimits'; export const getEnvInitValues = environmentConfigs => { let res = {}; @@ -278,3 +283,37 @@ export const transformAndSendConfigValues = ( return setConfig({ config: envs }); }; + +export const getLimitsInitValues = ( + limits, + tests, + environments, + exerciseId +) => { + let res = {}; + + tests.forEach(test => { + const testId = encodeTestName(test.name); + res[testId] = {}; + environments.forEach(environment => { + const envId = encodeEnvironmentId(environment.id); // the name can be anything, but it must be compatible with redux-form + const lim = limits.getIn([ + endpointDisguisedAsIdFactory({ + exerciseId, + runtimeEnvironmentId: environment.id + }), + 'data', + test.name + ]); + res[testId][envId] = lim + ? lim.toJS() + : { + // default + memory: 0, + 'wall-time': 0 + }; + }); + }); + + return { limits: res }; +}; diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index f1b680436..4df9168ba 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -19,9 +19,9 @@ import { fetchExerciseIfNeeded } from '../../redux/modules/exercises'; import { fetchExerciseEnvironmentSimpleLimitsIfNeeded, editEnvironmentSimpleLimits, - setHorizontally, - setVertically, - setAll + cloneHorizontally, + cloneVertically, + cloneAll } from '../../redux/modules/simpleLimits'; import { fetchExerciseConfigIfNeeded, @@ -32,7 +32,7 @@ import { exerciseConfigSelector } from '../../redux/selectors/exerciseConfigs'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments'; -import { simpleLimitsAllSelector } from '../../redux/selectors/simpleLimits'; +import { simpleLimitsSelector } from '../../redux/selectors/simpleLimits'; import withLinks from '../../hoc/withLinks'; import { getLocalizedName } from '../../helpers/getLocalizedData'; @@ -60,10 +60,11 @@ import { getTestsInitValues, transformAndSendTestsValues, getSimpleConfigInitValues, - transformAndSendConfigValues + transformAndSendConfigValues, + getLimitsInitValues } from '../../helpers/exerciseSimpleForm'; -const getLimitsInitValues = (limits, tests, environments) => { +const getLimitsInitValuesOld = (limits, tests, environments) => { let res = {}; tests.forEach(test => { @@ -131,9 +132,9 @@ class EditExerciseSimpleConfig extends Component { setConfig, limits, pipelines, - setHorizontally, - setVertically, - setAll, + cloneHorizontally, + cloneVertically, + cloneAll, intl: { locale } } = this.props; @@ -293,9 +294,7 @@ class EditExerciseSimpleConfig extends Component {
- - - - {prettyPrint(input.value)} - -
+ + + + {prettyPrint(input.value)} + +
- - - - {!isUniform && +const EditTestsTest = ({ fields, isUniform, testValues }) => { + const weightSum = isUniform + ? fields.length + : testValues.reduce((acc, val) => acc + Number(val.weight), 0); + + return ( +
+
- -
+ + } - - - - {fields.map((test, index) => - fields.remove(index)} + + {!isUniform && + } + + + + + {fields.map((test, index) => + fields.remove(index)} + /> + )} + +
- -
+ + + + +
+
+
- -
; + + + + ); +}; EditTestsTest.propTypes = { fields: PropTypes.object.isRequired, - isUniform: PropTypes.bool.isRequired + isUniform: PropTypes.bool.isRequired, + testValues: PropTypes.array.isRequired }; -export default EditTestsTest; +export default formValues({ + testValues: 'tests' +})(EditTestsTest); diff --git a/src/components/forms/EditTestsForm/EditTestsTestRow.js b/src/components/forms/EditTestsForm/EditTestsTestRow.js index 9ef8ddb6f..809e5a43d 100644 --- a/src/components/forms/EditTestsForm/EditTestsTestRow.js +++ b/src/components/forms/EditTestsForm/EditTestsTestRow.js @@ -8,7 +8,7 @@ import { FormattedMessage } from 'react-intl'; import { TextField } from '../Fields'; import './EditTests.css'; -const EditTestsTestRow = ({ test, onRemove, isUniform }) => +const EditTestsTestRow = ({ test, onRemove, isUniform, percent }) => groupClassName="testRow" /> } + + {percent} + {' '} + } + {
{' '} + } + ), submitting: ( ), success: ( ) }} @@ -80,8 +99,9 @@ class EditEnvironmentSimpleForm extends Component { EditEnvironmentSimpleForm.propTypes = { values: PropTypes.array, + reset: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, - anyTouched: PropTypes.bool, + dirty: PropTypes.bool, submitting: PropTypes.bool, hasFailed: PropTypes.bool, hasSucceeded: PropTypes.bool, diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index 081bea7d5..99a22b30b 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -7,6 +7,8 @@ import { reduxForm } from 'redux-form'; import { EditSimpleLimitsField } from '../Fields'; import SubmitButton from '../SubmitButton'; import FormBox from '../../widgets/FormBox'; +import Button from '../../widgets/FlatButton'; +import { RefreshIcon } from '../../icons'; import { encodeTestName, @@ -22,6 +24,7 @@ const EditSimpleLimitsForm = ({ cloneHorizontally, cloneVertically, cloneAll, + reset, handleSubmit, anyTouched, dirty, @@ -43,6 +46,21 @@ const EditSimpleLimitsForm = ({ dirty={dirty} footer={
+ {dirty && + + {' '} + } { @@ -17,7 +17,7 @@ const CheckboxField = ({ /* eslint-disable no-unneeded-ternary */ return ( @@ -25,12 +25,7 @@ const CheckboxField = ({ {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} } ); diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js index 9800fd49c..f47f208c8 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -93,7 +93,7 @@ const EditSimpleLimitsField = ({ label={ } validate={validateMemory} @@ -152,7 +152,7 @@ const EditSimpleLimitsField = ({ label={ } {...props} diff --git a/src/locales/cs.json b/src/locales/cs.json index 6391a576b..111055d9d 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -225,20 +225,20 @@ "app.editEnvironmentLimitsForm.validation.parallel.mustBePositive": "You must set the limit for the number of parallel processes to a positive number.", "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", - "app.editEnvironmentSimpleForm.failed": "Saving failed. Please try again later.", - "app.editEnvironmentSimpleForm.submit": "Change configuration", + "app.editEnvironmentSimpleForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", + "app.editEnvironmentSimpleForm.submit": "Uložit prostředí", "app.editEnvironmentSimpleForm.submitting": "Saving configuration ...", "app.editEnvironmentSimpleForm.success": "Configuration was changed.", "app.editEnvironmentSimpleForm.validation.environments": "Please add at least one runtime environment.", "app.editExercise.deleteExercise": "Smazat úlohu", "app.editExercise.deleteExerciseWarning": "Smazání úlohy odstraní všechna studentská řešení a všechna zadání této úlohy.", "app.editExercise.description": "Změna nastavení úlohy", - "app.editExercise.editConfig": "Edit exercise configuration", + "app.editExercise.editConfig": "Nastavení konfigurace úlohy", "app.editExercise.editEnvironmentConfig": "Upravit konfigurace prostředí", - "app.editExercise.editEnvironments": "Edit runtime environments", + "app.editExercise.editEnvironments": "Nastavení běhových prostředí", "app.editExercise.editScoreConfig": "Edit score configurations", - "app.editExercise.editTestConfig": "Upravit konfigurace", - "app.editExercise.editTests": "Edit tests", + "app.editExercise.editTestConfig": "Nastavení konfigurace", + "app.editExercise.editTests": "Nastavení testů", "app.editExercise.title": "Změna nastavení úlohy", "app.editExerciseConfig.description": "Change exercise configuration", "app.editExerciseConfig.title": "Edit exercise config", @@ -559,9 +559,9 @@ "app.feedbackAndBugs.title": "Feedback and Bugs Reporting", "app.feedbackAndBugs.whereToReportBugs": "Where can I report bugs?", "app.feedbackAndBugs.whereToReportBugsText": "Every software contains bugs and we are well avare of this fact. From time to time you might find a bug that nobody else has reported and which hasn't been fixed yet. Please report all bugs to our issue tracker on GitHub - just file a new issue and give it a label 'bug'. We will try to investigate and release a bugfix as soon as possible.", - "app.field.isRequired": "This field is required.", - "app.fields.limits.memory": "Paměťový limit [KiB]:", - "app.fields.limits.time": "Časový limit [s]:", + "app.field.isRequired": "Tato položka je povinná.", + "app.fields.limits.memory": "Paměť [KiB]:", + "app.fields.limits.time": "Čas [s]:", "app.filesTable.addFiles": "Save files", "app.filesTable.empty": "There are no uploaded files yet.", "app.filesTable.title": "Attached files", diff --git a/src/locales/en.json b/src/locales/en.json index 6a2c88c3f..95f9c2152 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -226,9 +226,9 @@ "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", "app.editEnvironmentSimpleForm.failed": "Saving failed. Please try again later.", - "app.editEnvironmentSimpleForm.submit": "Change configuration", - "app.editEnvironmentSimpleForm.submitting": "Saving configuration ...", - "app.editEnvironmentSimpleForm.success": "Configuration was changed.", + "app.editEnvironmentSimpleForm.submit": "Save Environments", + "app.editEnvironmentSimpleForm.submitting": "Saving Environments ...", + "app.editEnvironmentSimpleForm.success": "Environments Saved.", "app.editEnvironmentSimpleForm.validation.environments": "Please add at least one runtime environment.", "app.editExercise.deleteExercise": "Delete the exercise", "app.editExercise.deleteExerciseWarning": "Deleting an exercise will remove all the students submissions and all assignments.", @@ -560,8 +560,8 @@ "app.feedbackAndBugs.whereToReportBugs": "Where can I report bugs?", "app.feedbackAndBugs.whereToReportBugsText": "Every software contains bugs and we are well avare of this fact. From time to time you might find a bug that nobody else has reported and which hasn't been fixed yet. Please report all bugs to our issue tracker on GitHub - just file a new issue and give it a label 'bug'. We will try to investigate and release a bugfix as soon as possible.", "app.field.isRequired": "This field is required.", - "app.fields.limits.memory": "Memory Limit [KiB]:", - "app.fields.limits.time": "Time Limit [s]:", + "app.fields.limits.memory": "Memory [KiB]:", + "app.fields.limits.time": "Time [s]:", "app.filesTable.addFiles": "Save files", "app.filesTable.empty": "There are no uploaded files yet.", "app.filesTable.title": "Attached files", From 88f9edf9cb7243bba4c8fcde6bde7fc511cd7f33 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Fri, 8 Dec 2017 15:04:54 +0100 Subject: [PATCH 32/47] Refresh all data when environment configs are changed. --- src/helpers/exerciseSimpleForm.js | 7 +++- src/locales/cs.json | 22 +++++++---- src/locales/en.json | 22 +++++++---- .../EditExerciseSimpleConfig.js | 37 ++++++++++++++++--- 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 28cf94038..00063cb40 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -16,7 +16,8 @@ export const getEnvInitValues = environmentConfigs => { export const transformAndSendEnvValues = ( formData, environments, - editEnvironmentConfigs + editEnvironmentConfigs, + reloadConfigAndLimits ) => { let res = []; for (const env in formData) { @@ -28,7 +29,9 @@ export const transformAndSendEnvValues = ( envObj.variablesTable = currentFullEnv.defaultVariables; res.push(envObj); } - return editEnvironmentConfigs({ environmentConfigs: res }); + return editEnvironmentConfigs({ environmentConfigs: res }).then( + reloadConfigAndLimits + ); }; export const getTestsInitValues = (exerciseTests, scoreConfig, locale) => { diff --git a/src/locales/cs.json b/src/locales/cs.json index c259d3544..e0d926b23 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -76,6 +76,14 @@ "app.attachedFilesTable.addFiles": "Uložit soubory", "app.attachedFilesTable.empty": "Zatím zde nejsou žádné nahrané soubory.", "app.attachedFilesTable.title": "Přiložené soubory", + "app.attachmentFiles.deleteButton": "Delete", + "app.attachmentFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.attachmentFilesTable.description": "Attached files are files which can be used within exercise description using links provided below. Attached files can be viewed or downloaded by students.", + "app.attachmentFilesTable.fileName": "Original filename", + "app.attachmentFilesTable.fileSize": "Filesize", + "app.attachmentFilesTable.fileUploadedAt": "Uploaded at", + "app.attachmentFilesTable.title": "Attached files", + "app.attachmentFilesTable.url": "URL", "app.badge.failedLoading": "Nepodařilo se načíst data", "app.badge.failedLoadingInfo": "Prosim zkontrolujte si své připojení k Internetu.", "app.badge.loading": "Načítání ...", @@ -129,18 +137,11 @@ "app.confirm.no": "Ne", "app.confirm.yes": "Ano", "app.createGroup.externalId": "Externí identifikátor skupiny (například ID ze školního informačního systému):", - "app.createGroup.groupDescription": "Description:", - "app.createGroup.groupName": "Name:", "app.createGroup.isPublic": "Studenti se mohou sami přidávat k této skupině", "app.createGroup.publicStats": "Studenti mohou vidět dosažené body ostatních", "app.createGroup.threshold": "Minimální procentuální hranice potřebná ke splnění tohoto kurzu:", "app.createGroup.validation.thresholdBetweenZeroHundred": "Procentuální hranice musí být celé číslo od 0 do 100.", "app.createGroup.validation.thresholdMustBeInteger": "Procentuální hranice musí být celé číslo.", - "app.createGroupForm.createGroup": "Create new group", - "app.createGroupForm.failed": "We are sorry but we weren't able to create a new group.", - "app.createGroupForm.processing": "Group is being created ...", - "app.createGroupForm.success": "Group has been created", - "app.createGroupForm.title": "Create new group", "app.createGroupForm.validation.noLocalizedText": "Please add at least one localized text describing the group.", "app.dashboard.sisGroups": "SIS groups with ReCodEx mapping", "app.dashboard.studentOf": "Skupiny kde jste studentem", @@ -225,6 +226,7 @@ "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", "app.editEnvironmentSimpleForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", + "app.editEnvironmentSimpleForm.reset": "Reset", "app.editEnvironmentSimpleForm.submit": "Uložit prostředí", "app.editEnvironmentSimpleForm.submitting": "Saving configuration ...", "app.editEnvironmentSimpleForm.success": "Configuration was changed.", @@ -362,6 +364,7 @@ "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", "app.editSimpleLimitsForm.failed": "Cannot save the exercise limits. Please try again later.", + "app.editSimpleLimitsForm.reset": "Reset", "app.editSimpleLimitsForm.submit": "Save Limits", "app.editSimpleLimitsForm.submitting": "Saving Limits ...", "app.editSimpleLimitsForm.success": "Limits Saved", @@ -562,6 +565,9 @@ "app.field.isRequired": "Tato položka je povinná.", "app.fields.limits.memory": "Paměť [KiB]:", "app.fields.limits.time": "Čas [s]:", + "app.filesTable.addFiles": "Save files", + "app.filesTable.empty": "There are no uploaded files yet.", + "app.filesTable.title": "Attached files", "app.footer.copyright": "Copyright © 2016-2017 ReCodEx. Všechna práva vyhrazena.", "app.footer.version": "Verze {version}", "app.forkPipelineButton.success": "Show the forked pipeline", @@ -986,4 +992,4 @@ "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", "recodex-judge-shuffle-rows": "Unordered-rows judge" -} +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 029e063d8..bc5bde41d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -76,6 +76,14 @@ "app.attachedFilesTable.addFiles": "Save files", "app.attachedFilesTable.empty": "There are no uploaded files yet.", "app.attachedFilesTable.title": "Attached files", + "app.attachmentFiles.deleteButton": "Delete", + "app.attachmentFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.attachmentFilesTable.description": "Attached files are files which can be used within exercise description using links provided below. Attached files can be viewed or downloaded by students.", + "app.attachmentFilesTable.fileName": "Original filename", + "app.attachmentFilesTable.fileSize": "Filesize", + "app.attachmentFilesTable.fileUploadedAt": "Uploaded at", + "app.attachmentFilesTable.title": "Attached files", + "app.attachmentFilesTable.url": "URL", "app.badge.failedLoading": "Failed to load the data", "app.badge.failedLoadingInfo": "Please check your Internet connection.", "app.badge.loading": "Loading ...", @@ -129,18 +137,11 @@ "app.confirm.no": "No", "app.confirm.yes": "Yes", "app.createGroup.externalId": "External ID of the group (e. g. ID of the group in the school IS):", - "app.createGroup.groupDescription": "Description:", - "app.createGroup.groupName": "Name:", "app.createGroup.isPublic": "Students can join the group themselves", "app.createGroup.publicStats": "Students can see statistics of each other", "app.createGroup.threshold": "Minimum percent of the total points count needed to complete the course:", "app.createGroup.validation.thresholdBetweenZeroHundred": "Threshold must be an integer in between 0 and 100.", "app.createGroup.validation.thresholdMustBeInteger": "Threshold must be an integer.", - "app.createGroupForm.createGroup": "Create new group", - "app.createGroupForm.failed": "We are sorry but we weren't able to create a new group.", - "app.createGroupForm.processing": "Group is being created ...", - "app.createGroupForm.success": "Group has been created", - "app.createGroupForm.title": "Create new group", "app.createGroupForm.validation.noLocalizedText": "Please add at least one localized text describing the group.", "app.dashboard.sisGroups": "SIS groups with ReCodEx mapping", "app.dashboard.studentOf": "Groups you are student of", @@ -225,6 +226,7 @@ "app.editEnvironmentLimitsForm.validation.time": "You must set the time limit.", "app.editEnvironmentLimitsForm.validation.time.mustBePositive": "You must set the time limit to a positive number.", "app.editEnvironmentSimpleForm.failed": "Saving failed. Please try again later.", + "app.editEnvironmentSimpleForm.reset": "Reset", "app.editEnvironmentSimpleForm.submit": "Save Environments", "app.editEnvironmentSimpleForm.submitting": "Saving Environments ...", "app.editEnvironmentSimpleForm.success": "Environments Saved.", @@ -362,6 +364,7 @@ "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", "app.editSimpleLimitsForm.failed": "Cannot save the exercise limits. Please try again later.", + "app.editSimpleLimitsForm.reset": "Reset", "app.editSimpleLimitsForm.submit": "Save Limits", "app.editSimpleLimitsForm.submitting": "Saving Limits ...", "app.editSimpleLimitsForm.success": "Limits Saved", @@ -562,6 +565,9 @@ "app.field.isRequired": "This field is required.", "app.fields.limits.memory": "Memory [KiB]:", "app.fields.limits.time": "Time [s]:", + "app.filesTable.addFiles": "Save files", + "app.filesTable.empty": "There are no uploaded files yet.", + "app.filesTable.title": "Attached files", "app.footer.copyright": "Copyright © 2016-2017 ReCodEx. All rights reserved.", "app.footer.version": "Version {version}", "app.forkPipelineButton.success": "Show the forked pipeline", @@ -986,4 +992,4 @@ "recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge", "recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)", "recodex-judge-shuffle-rows": "Unordered-rows judge" -} +} \ No newline at end of file diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index c293d17bf..35aead148 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -15,7 +15,10 @@ import EditTestsForm from '../../components/forms/EditTestsForm'; import EditExerciseSimpleConfigForm from '../../components/forms/EditExerciseSimpleConfigForm'; import EditEnvironmentSimpleForm from '../../components/forms/EditEnvironmentSimpleForm'; -import { fetchExerciseIfNeeded } from '../../redux/modules/exercises'; +import { + fetchExercise, + fetchExerciseIfNeeded +} from '../../redux/modules/exercises'; import { fetchExerciseEnvironmentSimpleLimitsIfNeeded, editEnvironmentSimpleLimits, @@ -40,6 +43,7 @@ import withLinks from '../../hoc/withLinks'; import { getLocalizedName } from '../../helpers/getLocalizedData'; import { exerciseEnvironmentConfigSelector } from '../../redux/selectors/exerciseEnvironmentConfigs'; import { + fetchExerciseEnvironmentConfig, fetchExerciseEnvironmentConfigIfNeeded, setExerciseEnvironmentConfig } from '../../redux/modules/exerciseEnvironmentConfigs'; @@ -119,6 +123,7 @@ class EditExerciseSimpleConfig extends Component { cloneHorizontally, cloneVertically, cloneAll, + reloadConfigAndLimits, intl: { locale } } = this.props; @@ -220,7 +225,8 @@ class EditExerciseSimpleConfig extends Component { transformAndSendEnvValues( data, environments, - editEnvironmentConfigs + editEnvironmentConfigs, + reloadConfigAndLimits(exercise.id) )} />} @@ -274,9 +280,13 @@ class EditExerciseSimpleConfig extends Component { - {tests => + {(tests, envConfig) => transformAndSendLimitsValues( @@ -332,6 +342,7 @@ EditExerciseSimpleConfig.propTypes = { cloneHorizontally: PropTypes.func.isRequired, cloneVertically: PropTypes.func.isRequired, cloneAll: PropTypes.func.isRequired, + reloadConfigAndLimits: PropTypes.func.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; @@ -398,7 +409,23 @@ export default injectIntl( cloneHorizontally(formName, testName, runtimeEnvironmentId, field) ), cloneAll: (formName, testName, runtimeEnvironmentId) => field => () => - dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)) + dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)), + + reloadConfigAndLimits: exerciseId => () => + dispatch(fetchExercise(exerciseId)).then(({ value: exercise }) => + Promise.all([ + dispatch(fetchExerciseConfig(exerciseId)), + dispatch(fetchExerciseEnvironmentConfig(exerciseId)), + ...exercise.runtimeEnvironments.map(environment => + dispatch( + fetchExerciseEnvironmentSimpleLimits( + exerciseId, + environment.id + ) + ) + ) + ]) + ) }) )(EditExerciseSimpleConfig) ) From a773445327a15ce87b98b41558d3d817adc32860 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Fri, 8 Dec 2017 18:43:27 +0100 Subject: [PATCH 33/47] Additional forms polishing and fixes for simple bugs. --- .../EditExerciseSimpleConfigForm.js | 25 ++++- .../EditExerciseSimpleConfigTest.js | 1 - .../forms/EditTestsForm/EditTestsForm.js | 6 +- src/helpers/exerciseSimpleForm.js | 19 ++-- src/locales/cs.json | 33 +++--- src/locales/en.json | 5 +- .../EditExerciseSimpleConfig.js | 104 +++++++++++------- src/redux/modules/exercises.js | 10 +- 8 files changed, 131 insertions(+), 72 deletions(-) diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 611a1b097..45757969b 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -12,8 +12,11 @@ import SubmitButton from '../SubmitButton'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; +import Button from '../../widgets/FlatButton'; +import { RefreshIcon } from '../../icons'; + const EditExerciseSimpleConfigForm = ({ - anyTouched, + reset, handleSubmit, submitting, submitFailed, @@ -37,6 +40,22 @@ const EditExerciseSimpleConfigForm = ({ dirty={dirty} footer={
+ {dirty && + + {' '} + } + ), validating: ( @@ -101,8 +120,8 @@ const EditExerciseSimpleConfigForm = ({ EditExerciseSimpleConfigForm.propTypes = { initialValues: PropTypes.object, + reset: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, - anyTouched: PropTypes.bool, submitting: PropTypes.bool, hasFailed: PropTypes.bool, hasSucceeded: PropTypes.bool, diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index 5dc56dd1b..67d7f3748 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -217,7 +217,6 @@ const EditExerciseSimpleConfigTest = ({ name={`${test}.judgeBinary`} component={SelectField} options={[ - { key: '', name: '...' }, { key: 'recodex-judge-normal', name: intl.formatMessage(messages.normal) diff --git a/src/components/forms/EditTestsForm/EditTestsForm.js b/src/components/forms/EditTestsForm/EditTestsForm.js index f190fe12f..c44815f15 100644 --- a/src/components/forms/EditTestsForm/EditTestsForm.js +++ b/src/components/forms/EditTestsForm/EditTestsForm.js @@ -54,6 +54,8 @@ class EditTestsForm extends Component {
{dirty && + !submitting && + !hasSucceeded && + +
}
@@ -285,10 +339,12 @@ const EditExerciseSimpleConfigTest = ({ EditExerciseSimpleConfigTest.propTypes = { testName: PropTypes.string.isRequired, test: PropTypes.string.isRequired, - i: PropTypes.number.isRequired, + testKey: PropTypes.string.isRequired, + testIndex: PropTypes.number.isRequired, supplementaryFiles: PropTypes.array.isRequired, exerciseTests: PropTypes.array, formValues: PropTypes.object, + smartFill: PropTypes.func.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index c72d2512e..c560f8fe9 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -235,7 +235,7 @@ const validate = ({ limits }) => { export default reduxForm({ form: 'editSimpleLimits', enableReinitialize: true, - keepDirtyOnReinitialize: true, + keepDirtyOnReinitialize: false, immutableProps: [ 'environments', 'tests', diff --git a/src/components/forms/EditTestsForm/EditTests.css b/src/components/forms/EditTestsForm/EditTests.css index 2fa581e99..3a64352fa 100644 --- a/src/components/forms/EditTestsForm/EditTests.css +++ b/src/components/forms/EditTestsForm/EditTests.css @@ -1,4 +1,4 @@ .testRow { margin-bottom: 0; - margin-top: -20px; + margin-top: 0; } diff --git a/src/components/forms/EditTestsForm/EditTestsForm.js b/src/components/forms/EditTestsForm/EditTestsForm.js index c44815f15..5f2135cc7 100644 --- a/src/components/forms/EditTestsForm/EditTestsForm.js +++ b/src/components/forms/EditTestsForm/EditTestsForm.js @@ -78,7 +78,7 @@ class EditTestsForm extends Component { hasSucceeded={hasSucceeded} dirty={dirty} hasFailed={hasFailed} - handleSubmit={data => handleSubmit(data).then(reset)} + handleSubmit={handleSubmit} messages={{ submit: ( { reduxForm({ form: 'editTests', enableReinitialize: true, - keepDirtyOnReinitialize: true, + keepDirtyOnReinitialize: false, validate })(EditTestsForm) ); diff --git a/src/components/forms/EditTestsForm/EditTestsTest.js b/src/components/forms/EditTestsForm/EditTestsTest.js index 2ec97d62f..cb7c048ec 100644 --- a/src/components/forms/EditTestsForm/EditTestsTest.js +++ b/src/components/forms/EditTestsForm/EditTestsTest.js @@ -53,20 +53,24 @@ const EditTestsTest = ({ fields, isUniform, testValues }) => { )} -
- -
+ {fields.length < 99 && +
+ +
}
); }; diff --git a/src/components/forms/Fields/ExpandingInputFilesField.js b/src/components/forms/Fields/ExpandingInputFilesField.js index f421629e4..8a3a35851 100644 --- a/src/components/forms/Fields/ExpandingInputFilesField.js +++ b/src/components/forms/Fields/ExpandingInputFilesField.js @@ -1,170 +1,107 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import { - Row, - Col, - FormGroup, - FormControl, - HelpBlock, - ControlLabel -} from 'react-bootstrap'; -import classNames from 'classnames'; +import { Field } from 'redux-form'; +import { ControlLabel } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; -import styles from './commonStyles.less'; - -const EMPTY_VALUE = { first: '', second: '' }; - -class ExpandingInputFilesField extends Component { - state = { texts: [] }; - - componentDidMount() { - const { input: { value } } = this.props; - const initialValue = Array.isArray(value) - ? value.concat([EMPTY_VALUE]) - : [EMPTY_VALUE]; - this.setState({ texts: initialValue }); - } +import FlatButton from '../../widgets/FlatButton'; - changeText = (i, text, isFirst, onChange) => { - const { texts } = this.state; - if (isFirst) { - texts[i] = { first: text.trim(), second: texts[i].second }; - } else { - texts[i] = { first: texts[i].first, second: text.trim() }; - } - if (i === texts.length - 1) { - texts.push(EMPTY_VALUE); - } - this.setState({ texts }); +import SelectField from './SelectField'; +import TextField from './TextField'; - const texts2 = texts.slice(0, texts.length - 1); - onChange(texts2); - }; - - removeIfEmpty = (i, onChange) => { - const { texts } = this.state; - if ( - i !== texts.length - 1 && - texts[i].first === '' && - texts[i].second === '' - ) { - texts.splice(i, 1); - this.setState({ texts }); - - const texts2 = texts.slice(0, texts.length - 1); - onChange(texts2); - } - }; +import styles from './commonStyles.less'; - isReference = () => {}; +const EMPTY_VALUE = { file: '', name: '' }; - render() { - const { - leftLabel = '', - rightLabel = '', - input: { onChange, onFocus, onBlur, ...input }, - meta: { active, dirty, error, warning }, - style = {}, - options, - ignoreDirty = false, - ...props - } = this.props; - const { texts } = this.state; +const validate = value => + !value || value.trim() === '' + ? + : undefined; - return ( - - - - - {leftLabel} - -
- {texts.map((text, i) => - - this.changeText(i, e.target.value, true, onChange)} - onFocus={onFocus} - onBlur={e => { - onBlur(e); - this.removeIfEmpty(i, onChange); - }} - value={text.first} - componentClass="select" - bsClass={classNames({ - 'form-control': true, - [styles.dirty]: - i < texts.length - 1 && - dirty && - !ignoreDirty && - !error && - !warning, - [styles.active]: active - })} +const ExpandingInputFilesField = ({ + fields, + meta: { active, dirty, error, warning }, + leftLabel = '', + rightLabel = '', + options, + ...props +}) => +
+ {fields.length > 0 && + + + + + + + + + {fields.map((field, index) => + + - - {rightLabel} - -
- {texts.map((text, i) => - - this.changeText(i, e.target.value, false, onChange)} - onFocus={onFocus} - onBlur={e => { - onBlur(e); - this.removeIfEmpty(i, onChange); - }} - value={text.second} - bsClass={classNames({ - 'form-control': true, - [styles.dirty]: - i < texts.length - 1 && - dirty && - !ignoreDirty && - !error && - !warning, - [styles.active]: active - })} + /> + +
+ + + + )} + +
+ + {leftLabel} + + + + {rightLabel} + + + +
+ - {options.map(({ key, name }, o) => - - )} - - )} - - -
+ - )} - - - - {error && - - {' '}{error}{' '} - } - {!error && - warning && - - {' '}{warning}{' '} - } - - ); - } -} + + fields.insert(index, EMPTY_VALUE)}> + + + + fields.remove(index)}> + + +
} +
+ {fields.length === 0 && + + + } + fields.push(EMPTY_VALUE)}> + + +
+
; ExpandingInputFilesField.propTypes = { - input: PropTypes.object, + fields: PropTypes.object.isRequired, meta: PropTypes.shape({ active: PropTypes.bool, dirty: PropTypes.bool, @@ -179,9 +116,7 @@ ExpandingInputFilesField.propTypes = { PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) ]).isRequired, - style: PropTypes.object, - options: PropTypes.array, - ignoreDirty: PropTypes.bool + options: PropTypes.array }; export default ExpandingInputFilesField; diff --git a/src/components/forms/Fields/ExpandingTextField.js b/src/components/forms/Fields/ExpandingTextField.js index 658a862e2..a188ba8df 100644 --- a/src/components/forms/Fields/ExpandingTextField.js +++ b/src/components/forms/Fields/ExpandingTextField.js @@ -1,112 +1,63 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import { Field } from 'redux-form'; +import { ControlLabel } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; -import { - FormGroup, - FormControl, - HelpBlock, - ControlLabel -} from 'react-bootstrap'; -import classNames from 'classnames'; +import FlatButton from '../../widgets/FlatButton'; -import styles from './commonStyles.less'; - -class ExpandingTextField extends Component { - state = { texts: [] }; - - componentDidMount() { - const { input: { value } } = this.props; - const initialValue = Array.isArray(value) - ? value.concat(['']) - : value.trim() !== '' ? [value, ''] : ['']; - this.setState({ texts: initialValue }); - } - - changeText = (i, text, onChange) => { - const { texts } = this.state; - texts[i] = text.trim(); - if (i === texts.length - 1) { - texts.push(''); - } - this.setState({ texts }); - - const texts2 = texts.slice(0, texts.length - 1); - onChange(texts2); - }; +import TextField from './TextField'; - removeIfEmpty = (i, onChange) => { - const { texts } = this.state; - if (i !== texts.length - 1 && texts[i] === '') { - texts.splice(i, 1); - this.setState({ texts }); - - const texts2 = texts.slice(0, texts.length - 1); - onChange(texts2); - } - }; - - isReference = () => {}; - - render() { - const { - label = '', - input: { onChange, onFocus, onBlur }, - meta: { active, dirty, error, warning }, - style = {}, - ignoreDirty = false, - ...props - } = this.props; - const { texts } = this.state; +import styles from './commonStyles.less'; - return ( - - {label} -
- {texts.map((text, i) => - this.changeText(i, e.target.value, onChange)} - onFocus={onFocus} - onBlur={e => { - onBlur(e); - this.removeIfEmpty(i, onChange); - }} - value={text} - bsClass={classNames({ - 'form-control': true, - [styles.dirty]: - i < texts.length - 1 && - dirty && - !ignoreDirty && - !error && - !warning, - [styles.active]: active - })} - {...props} - /> - )} -
{' '} - {error && - - {' '}{error}{' '} - } - {!error && - warning && - - {' '}{warning}{' '} - } -
- ); - } -} +const ExpandingTextField = ({ + fields, + meta: { active, dirty, error, warning }, + label, + ...props +}) => +
+ + {label} + + + + {fields.map((field, index) => + + + + + + )} + +
+ + + fields.insert(index, '')}> + + + + fields.remove(index)}> + + +
+
+ {fields.length === 0 && + + + } + fields.push('')}> + + +
+
; ExpandingTextField.propTypes = { - input: PropTypes.object, + fields: PropTypes.object.isRequired, meta: PropTypes.shape({ active: PropTypes.bool, dirty: PropTypes.bool, @@ -116,9 +67,7 @@ ExpandingTextField.propTypes = { label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) - ]).isRequired, - style: PropTypes.object, - ignoreDirty: PropTypes.bool + ]).isRequired }; export default ExpandingTextField; diff --git a/src/components/forms/Fields/SelectField.js b/src/components/forms/Fields/SelectField.js index e199041ad..a2724dd35 100644 --- a/src/components/forms/Fields/SelectField.js +++ b/src/components/forms/Fields/SelectField.js @@ -26,9 +26,10 @@ const SelectField = ({ controlId={input.name} validationState={error ? 'error' : warning ? 'warning' : undefined} > - - {label} - + {label && + + {label} + } - - {label} - + {label && + + {label} + } { const confTests = - tests && config[0] && config[0].tests - ? config[0].tests.sort((a, b) => { - const aName = tests.find(test => test.id === a.name).name; - const bName = tests.find(test => test.id === b.name).name; - return aName.localeCompare(bName, locale); - }) - : []; + tests && config[0] && config[0].tests ? config[0].tests : []; - let res = []; - for (let test of confTests) { - let testObj = { name: test.name }; - const variables = test.pipelines.reduce( - (acc, pipeline) => acc.concat(pipeline.variables), - [] - ); + let res = {}; + for (let test of tests) { + const testConf = confTests.find(t => t.name === test.id); + let testObj = { name: test.id }; + + const variables = + testConf && testConf.pipelines + ? testConf.pipelines.reduce( + (acc, pipeline) => acc.concat(pipeline.variables), + [] + ) + : []; const inputFiles = variables.find( variable => variable.name === 'input-files' @@ -108,10 +107,10 @@ export const getSimpleConfigInitValues = (config, tests, locale) => { if (inputFiles) { testObj.inputFiles = inputFiles.value ? inputFiles.value.map((value, i) => ({ - first: value, - second: + file: value, + name: actualInputs && actualInputs.value && actualInputs.value[i] - ? actualInputs.value[i] + ? actualInputs.value[i].trim() : '' })) : []; @@ -125,17 +124,21 @@ export const getSimpleConfigInitValues = (config, tests, locale) => { } const runArgs = variables.find(variable => variable.name === 'run-args'); - if (runArgs) { - testObj.runArgs = runArgs.value; + testObj.runArgs = []; + if (runArgs && runArgs.value) { + testObj.runArgs = Array.isArray(runArgs.value) + ? runArgs.value + : [runArgs.value]; } const actualOutput = variables.find( variable => variable.name === 'actual-output' ); - if (actualOutput) { + if (actualOutput && actualOutput.value && actualOutput.value.trim()) { testObj.useOutFile = true; - testObj.outputFile = actualOutput.value; + testObj.outputFile = actualOutput.value.trim(); } else { + testObj.useOutFile = false; testObj.outputFile = ''; } @@ -154,9 +157,11 @@ export const getSimpleConfigInitValues = (config, tests, locale) => { ); testObj.useCustomJudge = false; - if (customJudge) { + testObj.customJudgeBinary = ''; + testObj.judgeBinary = ''; + if (customJudge && customJudge.value) { testObj.customJudgeBinary = customJudge.value; - testObj.useCustomJudge = customJudge.value.trim() !== ''; + testObj.useCustomJudge = true; } if (!testObj.useCustomJudge) { testObj.judgeBinary = @@ -168,16 +173,14 @@ export const getSimpleConfigInitValues = (config, tests, locale) => { const judgeArgs = variables.find( variable => variable.name === 'judge-args' ); - if (judgeArgs) { - testObj.judgeArgs = judgeArgs.value; + testObj.judgeArgs = []; + if (judgeArgs && judgeArgs.value) { + testObj.judgeArgs = Array.isArray(judgeArgs.value) + ? judgeArgs.value + : [judgeArgs.value]; } - res.push(testObj); - } - - // fill new tests with default judge - for (let i = confTests.length; i < tests.length; ++i) { - res.push({ judgeBinary: 'recodex-judge-normal' }); + res[encodeTestId(test.id)] = testObj; } return { config: res }; @@ -187,13 +190,13 @@ export const transformAndSendConfigValues = ( formData, pipelines, environments, - sortedTests, + tests, setConfig ) => { let testVars = []; - for (let testIndex = 0; testIndex < sortedTests.length; ++testIndex) { - const test = formData.config[testIndex]; - const testName = sortedTests[testIndex].id; + for (let t of tests) { + const testName = t.id; + const test = formData.config[encodeTestId(testName)]; let variables = []; variables.push({ @@ -229,8 +232,8 @@ export const transformAndSendConfigValues = ( if (test.useOutFile) { variables.push({ name: 'actual-output', - type: 'file[]', - value: test.outputFile + type: 'file', + value: test.useOutFile ? test.outputFile.trim() : '' }); } @@ -239,8 +242,8 @@ export const transformAndSendConfigValues = ( const inFilesArr = test.inputFiles && Array.isArray(test.inputFiles) ? test.inputFiles : []; for (const item of inFilesArr) { - inputFiles.push(item.first); - renamedNames.push(item.second); + inputFiles.push(item.file); + renamedNames.push(item.name.trim()); } variables.push({ name: 'input-files', @@ -267,7 +270,7 @@ export const transformAndSendConfigValues = ( pipeline => pipeline.runtimeEnvironmentIds.indexOf(envId) >= 0 ); - let tests = []; + let testsCfg = []; for (const testVar of testVars) { const compilationPipelineId = envPipelines.filter( pipeline => pipeline.parameters.isCompilationPipeline @@ -279,7 +282,7 @@ export const transformAndSendConfigValues = ( ? pipeline.parameters.producesFiles : pipeline.parameters.producesStdout) )[0].id; - tests.push({ + testsCfg.push({ name: testVar.name, pipelines: [ { @@ -295,7 +298,7 @@ export const transformAndSendConfigValues = ( } envs.push({ name: envId, - tests: tests + tests: testsCfg }); } diff --git a/src/locales/cs.json b/src/locales/cs.json index f2f4536fb..d660d4f58 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -251,6 +251,8 @@ "app.editExerciseConfigForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", "app.editExerciseConfigForm.pipelines": "Pipeliny", "app.editExerciseConfigForm.removeLastTest": "Odstranit poslední test", + "app.editExerciseConfigForm.smartFill": "Smart Fill", + "app.editExerciseConfigForm.smartFill.yesNoQuestion": "Do you really wish to overwrite configuration of all subsequent tests using the first test as a template? Files will be paired to individual test configurations by a heuristics based on matching name substrings.", "app.editExerciseConfigForm.submit": "Změnit konfiguraci", "app.editExerciseConfigForm.submitting": "Ukládání konfigurace ...", "app.editExerciseConfigForm.success": "Konfigurace byla uložena.", @@ -286,26 +288,26 @@ "app.editExerciseSimpleConfigForm.submitting": "Ukládám konfiguraci ...", "app.editExerciseSimpleConfigForm.success": "Konfigurace uložena", "app.editExerciseSimpleConfigForm.validating": "Validuji ...", - "app.editExerciseSimpleConfigForm.validation.customJudge": "Please select the custom judge binary for this test or use one of the standard judges instead.", - "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Please fill the expected output file.", - "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Input files are not properly paired with their names. Please make sure each file has a name.", - "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Please select the judge type for this test.", - "app.editExerciseSimpleConfigForm.validation.outputFile": "Please fill the name of the output file or use standard input instead.", - "app.editExerciseSimpleConfigTests.customJudgeBinary": "Custom judge binary:", - "app.editExerciseSimpleConfigTests.executionArguments": "Execution arguments:", - "app.editExerciseSimpleConfigTests.executionTitle": "Execution", - "app.editExerciseSimpleConfigTests.expectedOutput": "Expected output:", - "app.editExerciseSimpleConfigTests.inputFilesActual": "Input file:", - "app.editExerciseSimpleConfigTests.inputFilesRename": "Renamed file name:", - "app.editExerciseSimpleConfigTests.inputStdin": "Stdin:", - "app.editExerciseSimpleConfigTests.inputTitle": "Input", - "app.editExerciseSimpleConfigTests.judgeArgs": "Judge arguments:", - "app.editExerciseSimpleConfigTests.judgeBinary": "Judge binary:", - "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", - "app.editExerciseSimpleConfigTests.outputFile": "Output file:", - "app.editExerciseSimpleConfigTests.outputTitle": "Output", - "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", - "app.editExerciseSimpleConfigTests.useOutfile": "Use output file instead of stdout", + "app.editExerciseSimpleConfigForm.validation.customJudge": "Prosím, zvolte spustitelný soubor sudího, nebo si vyberte z předpřipravené množiny vestavěných sudí.", + "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Prosím zvolte soubor s očekávaným výstupem.", + "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Vstupní soubory nemají správně vyplněné položky přejmenování. Každý soubor musí mít své jméno.", + "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Prosím, vyberte sudího pro tento test.", + "app.editExerciseSimpleConfigForm.validation.outputFile": "Prosím, vyberte jméno výstupního souboru.", + "app.editExerciseSimpleConfigTests.customJudgeBinary": "Spustitelný soubor sudího:", + "app.editExerciseSimpleConfigTests.executionArguments": "Spouštěcí argumenty:", + "app.editExerciseSimpleConfigTests.executionTitle": "Spuštění", + "app.editExerciseSimpleConfigTests.expectedOutput": "Očekávaný výstup:", + "app.editExerciseSimpleConfigTests.inputFilesActual": "Vstupní soubor:", + "app.editExerciseSimpleConfigTests.inputFilesRename": "Přejmenování:", + "app.editExerciseSimpleConfigTests.inputStdin": "Std. vstup:", + "app.editExerciseSimpleConfigTests.inputTitle": "Vstup", + "app.editExerciseSimpleConfigTests.judgeArgs": "Argumenty sudího:", + "app.editExerciseSimpleConfigTests.judgeTitle": "Sudí", + "app.editExerciseSimpleConfigTests.judgeType": "Sudí:", + "app.editExerciseSimpleConfigTests.outputFile": "Výstupní soubor:", + "app.editExerciseSimpleConfigTests.outputTitle": "Výstup", + "app.editExerciseSimpleConfigTests.useCustomJudge": "Použít vlastní soubor sudího", + "app.editExerciseSimpleConfigTests.useOutfile": "Použít výstupní soubor místo std. výstupu", "app.editGroup.cannotDeleteRootGroup": "Toto je primární skupina a jako taková nemůže být smazána.", "app.editGroup.deleteGroup": "Smazat skupinu", "app.editGroup.deleteGroupWarning": "Smazání skupiny odstraní také všechny podskupiny, skupinové úlohy a zadané úlohy včetně studentských řešení.", @@ -544,6 +546,7 @@ "app.exitCodes.mono.201": "No main method", "app.exitCodes.mono.202": "More main methods", "app.exitCodes.unknown": "Unknown", + "app.expandingTextField.noItems": "There are no items yet...", "app.externalRegistrationForm.failed": "Registrace se nezdařila. Prosíme, zkontrolujte vyplněné informace.", "app.externalRegistrationForm.instance": "Instance:", "app.externalRegistrationForm.password": "Heslo:", diff --git a/src/locales/en.json b/src/locales/en.json index 3b0bc6ce5..26606ad21 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -251,6 +251,8 @@ "app.editExerciseConfigForm.failed": "Saving failed. Please try again later.", "app.editExerciseConfigForm.pipelines": "Pipelines", "app.editExerciseConfigForm.removeLastTest": "Remove last test", + "app.editExerciseConfigForm.smartFill": "Smart Fill", + "app.editExerciseConfigForm.smartFill.yesNoQuestion": "Do you really wish to overwrite configuration of all subsequent tests using the first test as a template? Files will be paired to individual test configurations by a heuristics based on matching name substrings.", "app.editExerciseConfigForm.submit": "Change configuration", "app.editExerciseConfigForm.submitting": "Saving configuration ...", "app.editExerciseConfigForm.success": "Configuration was changed.", @@ -286,22 +288,22 @@ "app.editExerciseSimpleConfigForm.submitting": "Saving Configuration ...", "app.editExerciseSimpleConfigForm.success": "Configuration Saved.", "app.editExerciseSimpleConfigForm.validating": "Validating ...", - "app.editExerciseSimpleConfigForm.validation.customJudge": "Please select the custom judge binary for this test or use one of the standard judges instead.", - "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Please fill the expected output file.", - "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Input files are not properly paired with their names. Please make sure each file has a name.", - "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Please select the judge type for this test.", - "app.editExerciseSimpleConfigForm.validation.outputFile": "Please fill the name of the output file or use standard input instead.", - "app.editExerciseSimpleConfigTests.customJudgeBinary": "Custom judge binary:", + "app.editExerciseSimpleConfigForm.validation.customJudge": "Please, select the custom judge binary for this test or use one of the standard judges instead.", + "app.editExerciseSimpleConfigForm.validation.expectedOutput": "Please, fill in the expected output file.", + "app.editExerciseSimpleConfigForm.validation.inputFilesNotPaired": "Input files are not properly paired with their names. Please, make sure each file has a name.", + "app.editExerciseSimpleConfigForm.validation.judgeBinary": "Please, select the judge type for this test.", + "app.editExerciseSimpleConfigForm.validation.outputFile": "Please, fill in the name of the output file.", + "app.editExerciseSimpleConfigTests.customJudgeBinary": "Custom judge executable:", "app.editExerciseSimpleConfigTests.executionArguments": "Execution arguments:", "app.editExerciseSimpleConfigTests.executionTitle": "Execution", "app.editExerciseSimpleConfigTests.expectedOutput": "Expected output:", "app.editExerciseSimpleConfigTests.inputFilesActual": "Input file:", - "app.editExerciseSimpleConfigTests.inputFilesRename": "Renamed file name:", + "app.editExerciseSimpleConfigTests.inputFilesRename": "Rename as:", "app.editExerciseSimpleConfigTests.inputStdin": "Stdin:", "app.editExerciseSimpleConfigTests.inputTitle": "Input", "app.editExerciseSimpleConfigTests.judgeArgs": "Judge arguments:", - "app.editExerciseSimpleConfigTests.judgeBinary": "Judge binary:", "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", + "app.editExerciseSimpleConfigTests.judgeType": "Judge:", "app.editExerciseSimpleConfigTests.outputFile": "Output file:", "app.editExerciseSimpleConfigTests.outputTitle": "Output", "app.editExerciseSimpleConfigTests.useCustomJudge": "Use custom judge binary", @@ -544,6 +546,7 @@ "app.exitCodes.mono.201": "No main method", "app.exitCodes.mono.202": "More main methods", "app.exitCodes.unknown": "Unknown", + "app.expandingTextField.noItems": "There are no items yet...", "app.externalRegistrationForm.failed": "Registration failed. Please check your information.", "app.externalRegistrationForm.instance": "Instance:", "app.externalRegistrationForm.password": "Password:", From 32892613b6e16ca4b0813e0d5f42e64aaafff319 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 14 Dec 2017 10:47:23 +0100 Subject: [PATCH 41/47] Update icons --- package.json | 2 +- .../TestResultsTable/TestResultsTable.js | 4 +- views/index.ejs | 38 +++++++++++-------- yarn.lock | 2 +- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 5f70a2dcd..403dad055 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "react-datetime": "^2.8.10", "react-dom": "^16.2.0", "react-dropzone": "^3.5.3", - "react-fontawesome": "^1.1.0", + "react-fontawesome": "1.6.1", "react-height": "^3.0.0", "react-helmet": "^5.0.3", "react-immutable-proptypes": "^2.1.0", diff --git a/src/components/Submissions/TestResultsTable/TestResultsTable.js b/src/components/Submissions/TestResultsTable/TestResultsTable.js index 5da3cdfb0..baafe8e8d 100644 --- a/src/components/Submissions/TestResultsTable/TestResultsTable.js +++ b/src/components/Submissions/TestResultsTable/TestResultsTable.js @@ -88,7 +88,7 @@ const TestResultsTable = ({ results, runtimeEnvironmentId }) => } > - + @@ -103,7 +103,7 @@ const TestResultsTable = ({ results, runtimeEnvironmentId }) => } > - + diff --git a/views/index.ejs b/views/index.ejs index b793dec94..7354082f0 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -1,24 +1,30 @@ - > - - <%- head.title.toString() %> + +> + + <%- head.title.toString() %> <%- head.meta.toString() %> - <%- head.link.toString() %> - - + <%- head.link.toString() %> + + + + + + + - - - - - -
<%- html %>
- <% if (reduxState) { %> + +
+ <%- html %> +
+ <% if (reduxState) { %> <% } %> - - + + + diff --git a/yarn.lock b/yarn.lock index 82520ab97..fcecacb76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5918,7 +5918,7 @@ react-dropzone@^3.5.3: attr-accept "^1.0.3" prop-types "^15.5.7" -react-fontawesome@^1.1.0: +react-fontawesome@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/react-fontawesome/-/react-fontawesome-1.6.1.tgz#eddce17e7dc731aa09fd4a186688a61793a16c5c" dependencies: From 9a1c1fb6bb154da3c2033ce2b3989cf5f2cc8b5a Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 14 Dec 2017 10:58:06 +0100 Subject: [PATCH 42/47] Fix linter and tests --- src/components/forms/Fields/CheckboxField.js | 2 -- src/components/forms/RegistrationForm/RegistrationForm.js | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/forms/Fields/CheckboxField.js b/src/components/forms/Fields/CheckboxField.js index 1267836e9..15f64d555 100644 --- a/src/components/forms/Fields/CheckboxField.js +++ b/src/components/forms/Fields/CheckboxField.js @@ -6,8 +6,6 @@ import { FormGroup, HelpBlock, Checkbox } from 'react-bootstrap'; import OnOffCheckbox from '../OnOffCheckbox'; -import styles from './commonStyles.less'; - const CheckboxField = ({ input, onOff = false, diff --git a/src/components/forms/RegistrationForm/RegistrationForm.js b/src/components/forms/RegistrationForm/RegistrationForm.js index c038edef7..41435c640 100644 --- a/src/components/forms/RegistrationForm/RegistrationForm.js +++ b/src/components/forms/RegistrationForm/RegistrationForm.js @@ -6,6 +6,7 @@ import { Alert } from 'react-bootstrap'; import isEmail from 'validator/lib/isEmail'; import ResourceRenderer from '../../helpers/ResourceRenderer'; +import { eventAggregator } from '../../../helpers/eventAggregator'; import FormBox from '../../widgets/FormBox'; import { EmailField, From 88273fa13a0b31d4f45db24cddfa17d54e129bc3 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 14 Dec 2017 11:34:25 +0100 Subject: [PATCH 43/47] Smart fill button is now operational. --- .../EditExerciseSimpleConfigForm.js | 89 +++++- .../EditExerciseSimpleConfigTest.js | 12 +- src/locales/cs.json | 5 +- src/locales/en.json | 5 +- src/redux/modules/exerciseConfigs.js | 278 ++++++++++++++++++ src/redux/selectors/exerciseConfigs.js | 6 + 6 files changed, 376 insertions(+), 19 deletions(-) diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 20a701843..c804b2212 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -15,6 +15,8 @@ import ResourceRenderer from '../../helpers/ResourceRenderer'; import EditExerciseSimpleConfigTest from './EditExerciseSimpleConfigTest'; import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; import { encodeTestId } from '../../../redux/modules/simpleLimits'; +import { smartFillExerciseConfigForm } from '../../../redux/modules/exerciseConfigs'; +import { exerciseConfigFormErrors } from '../../../redux/selectors/exerciseConfigs'; const EditExerciseSimpleConfigForm = ({ reset, @@ -25,8 +27,10 @@ const EditExerciseSimpleConfigForm = ({ invalid, dirty, formValues, + formErrors, supplementaryFiles, - exerciseTests + exerciseTests, + smartFill }) => undefined} + testErrors={formErrors && formErrors[encodeTestId(test.id)]} + smartFill={smartFill(test.id, exerciseTests, files)} /> )}
} @@ -133,21 +139,77 @@ EditExerciseSimpleConfigForm.propTypes = { submitSucceeded: PropTypes.bool, invalid: PropTypes.bool, formValues: PropTypes.object, + formErrors: PropTypes.object, supplementaryFiles: ImmutablePropTypes.map, - exerciseTests: PropTypes.array + exerciseTests: PropTypes.array, + smartFill: PropTypes.func.isRequired }; -export default connect((state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); - return { - supplementaryFiles: getSupplementaryFilesForExercise(state), - formValues: getFormValues('editExerciseSimpleConfig')(state) - }; -})( +const FORM_NAME = 'editExerciseSimpleConfig'; + +const validate = formData => { + const testErrors = {}; + + for (const testKey in formData.config) { + const test = formData.config[testKey]; + if (test.inputFiles.length > 1) { + // Construct a name index to detect duplicates ... + const nameIndex = {}; + test.inputFiles.forEach(({ name }, idx) => { + name = name && name.trim(); + if (name) { + if (nameIndex[name] === undefined) { + nameIndex[name] = [idx]; + } else { + nameIndex[name].push(idx); + } + } + }); + + // Traverse the index and place an error to all duplicates ... + for (const name in nameIndex) { + const indices = nameIndex[name]; + if (indices.length > 1) { + if (!testErrors[testKey]) { + testErrors[testKey] = { inputFiles: [] }; + } + indices.forEach( + idx => + (testErrors[testKey].inputFiles[idx] = ( + + )) + ); + console.log(testErrors); + } + } + } + } + return Object.keys(testErrors).length > 0 + ? { config: testErrors } + : undefined; +}; + +export default connect( + (state, { exercise }) => { + const getSupplementaryFilesForExercise = createGetSupplementaryFiles( + exercise.supplementaryFilesIds + ); + return { + supplementaryFiles: getSupplementaryFilesForExercise(state), + formValues: getFormValues(FORM_NAME)(state), + formErrors: exerciseConfigFormErrors(state, FORM_NAME) + }; + }, + dispatch => ({ + smartFill: (testId, tests, files) => () => + dispatch(smartFillExerciseConfigForm(FORM_NAME, testId, tests, files)) + }) +)( reduxForm({ - form: 'editExerciseSimpleConfig', + form: FORM_NAME, enableReinitialize: true, keepDirtyOnReinitialize: false, immutableProps: [ @@ -156,5 +218,6 @@ export default connect((state, { exercise }) => { 'exerciseTests', 'handleSubmit' ] + //validate })(EditExerciseSimpleConfigForm) ); diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js index 7fb896ff5..ccff65d55 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -85,14 +85,16 @@ const EditExerciseSimpleConfigTest = ({ formValues, testName, test, + testId, testKey, testIndex, + testErrors, smartFill, intl }) => { const supplementaryFilesOptions = supplementaryFiles .sort((a, b) => a.name.localeCompare(b.name, intl.locale)) - .filter((item, pos, arr) => arr.indexOf(item) === pos) + .filter((item, pos, arr) => arr.indexOf(item) === pos) // WTF? .map(data => ({ key: data.name, name: data.name @@ -321,7 +323,11 @@ const EditExerciseSimpleConfigTest = ({ /> } > -
); diff --git a/src/pages/Submission/Submission.js b/src/pages/Submission/Submission.js index b647febf2..b18754725 100644 --- a/src/pages/Submission/Submission.js +++ b/src/pages/Submission/Submission.js @@ -122,11 +122,13 @@ class Submission extends Component { id={submission.id} assignmentId={assignment.id} isDebug={false} + userId={submission.solution.userId} />

} From 8c7a2bbd8d3c484fc88d1fd5e42f9d080089e93e Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 14 Dec 2017 15:29:41 +0100 Subject: [PATCH 46/47] Fix linter --- src/redux/modules/exerciseConfigs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redux/modules/exerciseConfigs.js b/src/redux/modules/exerciseConfigs.js index a182c000e..fd76e4f39 100644 --- a/src/redux/modules/exerciseConfigs.js +++ b/src/redux/modules/exerciseConfigs.js @@ -3,7 +3,6 @@ import { change } from 'redux-form'; import factory, { initialState } from '../helpers/resourceManager'; import { encodeTestId } from './simpleLimits'; -import { absolute } from '../../links/index'; /** * Create actions & reducer From a8a5d4889ac54c0be28aa7f16a21b4c455dbfcd6 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 14 Dec 2017 16:47:09 +0100 Subject: [PATCH 47/47] Adding tooltips to some buttons. --- .../forms/Fields/EditSimpleLimitsField.js | 114 ++++++++++++++---- .../forms/Fields/ExpandingInputFilesField.js | 45 +++++-- .../forms/Fields/ExpandingTextField.js | 56 +++++++-- .../RegistrationForm/RegistrationForm.js | 2 - src/locales/cs.json | 8 ++ src/locales/en.json | 8 ++ 6 files changed, 187 insertions(+), 46 deletions(-) diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js index f47f208c8..99d86b61a 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field } from 'redux-form'; import { FormattedMessage } from 'react-intl'; -import { Row, Col } from 'react-bootstrap'; +import { Row, Col, OverlayTrigger, Tooltip } from 'react-bootstrap'; import Icon from 'react-fontawesome'; import FlatButton from '../../widgets/FlatButton'; @@ -102,9 +102,21 @@ const EditSimpleLimitsField = ({ {testsCount > 1 && - - - } + + + + } + > + + + + } {environmentsCount > 1 && } > - - - + + + + } + > + + + + } {testsCount > 1 && environmentsCount > 1 && @@ -132,9 +156,21 @@ const EditSimpleLimitsField = ({ /> } > - - - + + + + } + > + + + + } @@ -160,12 +196,24 @@ const EditSimpleLimitsField = ({ {testsCount > 1 && - + + + } > - - } + + + + } {environmentsCount > 1 && } > - - - + + + + } + > + + + + } {testsCount > 1 && environmentsCount > 1 && @@ -193,9 +253,21 @@ const EditSimpleLimitsField = ({ /> } > - - - + + + + } + > + + + + } diff --git a/src/components/forms/Fields/ExpandingInputFilesField.js b/src/components/forms/Fields/ExpandingInputFilesField.js index 8a3a35851..711dd881e 100644 --- a/src/components/forms/Fields/ExpandingInputFilesField.js +++ b/src/components/forms/Fields/ExpandingInputFilesField.js @@ -2,7 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { Field } from 'redux-form'; -import { ControlLabel } from 'react-bootstrap'; +import { ControlLabel, OverlayTrigger, Tooltip } from 'react-bootstrap'; + import Icon from 'react-fontawesome'; import FlatButton from '../../widgets/FlatButton'; @@ -46,7 +47,6 @@ const ExpandingInputFilesField = ({ - @@ -73,14 +73,21 @@ const ExpandingInputFilesField = ({ /> - fields.insert(index, EMPTY_VALUE)}> - - - - - fields.remove(index)}> - - + + + + } + > + fields.remove(index)}> + + + )} @@ -94,9 +101,21 @@ const ExpandingInputFilesField = ({ defaultMessage="There are no files yet..." /> } - fields.push(EMPTY_VALUE)}> - - + + + + } + > + fields.push(EMPTY_VALUE)}> + + + ; diff --git a/src/components/forms/Fields/ExpandingTextField.js b/src/components/forms/Fields/ExpandingTextField.js index a188ba8df..859b0be0c 100644 --- a/src/components/forms/Fields/ExpandingTextField.js +++ b/src/components/forms/Fields/ExpandingTextField.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { Field } from 'redux-form'; -import { ControlLabel } from 'react-bootstrap'; +import { ControlLabel, OverlayTrigger, Tooltip } from 'react-bootstrap'; import Icon from 'react-fontawesome'; import FlatButton from '../../widgets/FlatButton'; @@ -29,14 +29,38 @@ const ExpandingTextField = ({ - fields.insert(index, '')}> - - + + + + } + > + fields.insert(index, '')}> + + + - fields.remove(index)}> - - + + + + } + > + fields.remove(index)}> + + + )} @@ -50,9 +74,21 @@ const ExpandingTextField = ({ defaultMessage="There are no items yet..." /> } - fields.push('')}> - - + + + + } + > + fields.push('')}> + + + ; diff --git a/src/components/forms/RegistrationForm/RegistrationForm.js b/src/components/forms/RegistrationForm/RegistrationForm.js index 41435c640..ffdd9be7e 100644 --- a/src/components/forms/RegistrationForm/RegistrationForm.js +++ b/src/components/forms/RegistrationForm/RegistrationForm.js @@ -111,8 +111,6 @@ const RegistrationForm = ({ } /> - {/* */} -