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/package.json b/package.json index aa3129e5f..403dad055 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ }, "dependencies": { "admin-lte": "2.3.11", + "ajv": "5.5.1", + "ajv-keywords": "2.1.1", "babel-plugin-react-intl": "^2.1.3", "bluebird": "^3.3.5", "browser-cookies": "^1.0.8", @@ -48,39 +50,35 @@ "global": "^4.3.1", "immutable": "^3.8.1", "isomorphic-fetch": "^2.2.1", + "js-yaml": "^3.10.0", "jwt-decode": "^2.0.1", "moment": "^2.18.1", - "pretty-bytes": "^3.0.1", "pretty-ms": "^2.1.0", "prop-types": "^15.5.8", - "react": "^15.5.4", - "react-ace": "^4.2.1", - "react-bootstrap": "^0.31.2", - "react-codemirror": "^0.3.0", + "react": "^16.2.0", + "react-ace": "5.8.0", + "react-bootstrap": "0.31.5", "react-collapse": "^4.0.2", "react-copy-to-clipboard": "^5.0.1", "react-datetime": "^2.8.10", - "react-dom": "^15.5.4", + "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", - "react-intl": "^2.1.3", - "react-motion": "^0.5.0", + "react-intl": "2.4.0", + "react-motion": "^0.5.2", "react-redux": "^5.0.4", "react-remarkable": "^1.1.2", "react-responsive": "^1.1.3", - "react-router": "^3.0.0", + "react-router": "3.2.0", "react-router-bootstrap": "^0.23.3", "react-router-redux": "^4.0.8", - "react-rte": "^0.11.0", - "react-table": "^6.0.0", - "react-throttle": "^0.3.0", - "react-toggle": "^3.0.1", + "react-toggle": "4.0.2", "redux": "^3.5.1", "redux-actions": "^2.0.3", - "redux-form": "^6.2.1", + "redux-form": "7.2.0", "redux-promise-middleware": "^4.2.0", "redux-storage": "^4.1.2", "redux-storage-decorator-filter": "^1.1.8", @@ -118,7 +116,7 @@ "eslint-plugin-promise": "^3.5.0", "eslint-plugin-react": "^7.0.0", "eslint-plugin-standard": "^3.0.1", - "extract-text-webpack-plugin": "^2.0.0-beta.4", + "extract-text-webpack-plugin": "3.0.2", "fetch-mock": "^5.10.0", "file-loader": "^0.11.1", "git-revision-webpack-plugin": "^2.5.1", @@ -143,9 +141,9 @@ "scroll-behavior": "^0.4.0", "strip-loader": "^0.1.2", "style-loader": "^0.17.0", - "webpack": "^2.5.1", + "webpack": "3.10.0", "webpack-dev-middleware": "^1.10.2", - "webpack-dev-server": "^2.4.5", + "webpack-dev-server": "2.9.7", "webpack-isomorphic-tools": "^3.0.2" } } 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/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/Exercises/FilesTable/AttachmentFilesTableRow.js b/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js index 38771bc44..6583449b2 100644 --- a/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js +++ b/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import prettyBytes from 'pretty-bytes'; +import { prettyPrintBytes } from '../../helpers/stringFormatters'; import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; import withLinks from '../../../hoc/withLinks'; import { Button } from 'react-bootstrap'; @@ -25,7 +25,7 @@ const AttachmentFilesTableRow = ({ - {prettyBytes(size)} + {prettyPrintBytes(size)} diff --git a/src/components/Exercises/FilesTable/FilesTable.js b/src/components/Exercises/FilesTable/FilesTable.js index 404ca3cbf..7baa07d8e 100644 --- a/src/components/Exercises/FilesTable/FilesTable.js +++ b/src/components/Exercises/FilesTable/FilesTable.js @@ -31,7 +31,7 @@ const FilesTable = ({ RowComponent, intl }) => - +
{description &&

diff --git a/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js b/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js index 8035f17ca..3626f472e 100644 --- a/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js +++ b/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import prettyBytes from 'pretty-bytes'; +import { prettyPrintBytes } from '../../helpers/stringFormatters'; import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; import { Button } from 'react-bootstrap'; import Confirm from '../../../components/forms/Confirm'; @@ -26,7 +26,7 @@ const SupplementaryFilesTableRow = ({ } - {prettyBytes(size)} + {prettyPrintBytes(size)} 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 = ({ {' '} + } + + + ), + submitting: ( + + ), + success: ( + + ) + }} + /> +
+
+ ); + } +} + +EditEnvironmentSimpleForm.propTypes = { + values: PropTypes.array, + reset: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + dirty: PropTypes.bool, + submitting: PropTypes.bool, + hasFailed: PropTypes.bool, + hasSucceeded: PropTypes.bool, + invalid: PropTypes.bool, + runtimeEnvironments: PropTypes.array +}; + +const validate = formData => { + const errors = {}; + + if ( + Object.values(formData).filter(value => value === true || value === 'true') + .length === 0 + ) { + errors['_error'] = ( + + ); + } + + return errors; +}; + +export default reduxForm({ + form: 'editEnvironmentSimple', + validate +})(EditEnvironmentSimpleForm); diff --git a/src/components/forms/EditEnvironmentSimpleForm/index.js b/src/components/forms/EditEnvironmentSimpleForm/index.js new file mode 100644 index 000000000..ffdeffb1c --- /dev/null +++ b/src/components/forms/EditEnvironmentSimpleForm/index.js @@ -0,0 +1 @@ +export default from './EditEnvironmentSimpleForm'; diff --git a/src/components/forms/EditExerciseForm/EditExerciseForm.js b/src/components/forms/EditExerciseForm/EditExerciseForm.js index 81be6c89f..f0ffe2735 100644 --- a/src/components/forms/EditExerciseForm/EditExerciseForm.js +++ b/src/components/forms/EditExerciseForm/EditExerciseForm.js @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { canUseDOM } from 'exenv'; import { reduxForm, Field, FieldArray, touch } from 'redux-form'; import { injectIntl, @@ -22,10 +21,6 @@ import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import { validateExercise } from '../../../redux/modules/exercises'; import withLinks from '../../../hoc/withLinks'; -if (canUseDOM) { - require('codemirror/mode/yaml/yaml'); -} - const messages = defineMessages({ easy: { id: 'app.editExerciseForm.easy', @@ -52,7 +47,7 @@ const EditExerciseForm = ({ asyncValidating, formValues: { localizedTexts } = {}, intl: { formatMessage, locale }, - links: { EXERCISE_EDIT_CONFIG_URI_FACTORY } + links: { EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY } }) => - + {' '} + } + + + ), + submitting: ( + + ), + success: ( + + ), + validating: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + {(...files) => +
+ {exerciseTests.map((test, idx) => + + )} +
} +
+ ; + +EditExerciseSimpleConfigForm.propTypes = { + initialValues: PropTypes.object, + reset: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool, + hasFailed: PropTypes.bool, + hasSucceeded: PropTypes.bool, + dirty: PropTypes.bool, + submitFailed: PropTypes.bool, + submitSucceeded: PropTypes.bool, + invalid: PropTypes.bool, + formValues: PropTypes.object, + formErrors: PropTypes.object, + supplementaryFiles: ImmutablePropTypes.map, + exerciseTests: PropTypes.array, + smartFill: PropTypes.func.isRequired +}; + +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] = { + name: ( + + ) + }) + ); + } + } + } + } + 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: FORM_NAME, + enableReinitialize: true, + keepDirtyOnReinitialize: false, + immutableProps: [ + 'formValues', + 'supplementaryFiles', + 'exerciseTests', + 'handleSubmit' + ], + validate + })(EditExerciseSimpleConfigForm) +); diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js new file mode 100644 index 000000000..ccff65d55 --- /dev/null +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTest.js @@ -0,0 +1,359 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, FieldArray } from 'redux-form'; +import { Row, Col } from 'react-bootstrap'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import Icon from 'react-fontawesome'; + +import Button from '../../widgets/FlatButton'; +import { + SelectField, + TextField, + ExpandingTextField, + ExpandingInputFilesField, + CheckboxField +} from '../Fields'; +import Confirm from '../../forms/Confirm'; + +import './EditExerciseSimpleConfigForm.css'; + +const messages = defineMessages({ + normal: { + id: 'recodex-judge-normal', + defaultMessage: 'Token judge' + }, + float: { + id: 'recodex-judge-float', + defaultMessage: 'Float-numbers judge' + }, + normalNewline: { + id: 'recodex-judge-normal-newline', + defaultMessage: 'Token judge (ignoring ends of lines)' + }, + floatNewline: { + id: 'recodex-judge-float-newline', + defaultMessage: 'Float-numbers judge (ignoring ends of lines)' + }, + shuffle: { + id: 'recodex-judge-shuffle', + defaultMessage: 'Unordered-tokens judge' + }, + shuffleRows: { + id: 'recodex-judge-shuffle-rows', + defaultMessage: 'Unordered-rows judge' + }, + shuffleAll: { + id: 'recodex-judge-shuffle-all', + defaultMessage: 'Unordered-tokens-and-rows judge' + }, + shuffleNewline: { + id: 'recodex-judge-shuffle-newline', + defaultMessage: 'Unordered-tokens judge (ignoring ends of lines)' + }, + diff: { + id: 'diff', + defaultMessage: 'Binary-safe judge' + } +}); + +const validateExpectedOutput = value => + !value || value.trim() === '' + ? + : undefined; + +const validateOutputFile = value => + !value || value.trim() === '' + ? + : undefined; + +const validateCustomJudge = value => + !value || value.trim() === '' + ? + : undefined; + +const EditExerciseSimpleConfigTest = ({ + supplementaryFiles, + 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) // WTF? + .map(data => ({ + key: data.name, + name: data.name + })); + return ( +
+ + +

+ {testName} +

+ +
+ + +

+ +

+ + } + rightLabel={ + + } + /> + + } + /> + + +

+ +

+ + } + /> + + +

+ +

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

+ +

+ + } + /> + {formValues && + formValues.config && + formValues.config[testKey] && + (formValues.config[testKey].useCustomJudge === true || + formValues.config[testKey].useCustomJudge === 'true') + ? + } + /> + : + } + />} + {formValues && + formValues.config && + formValues.config[testKey] && + (formValues.config[testKey].useCustomJudge === true || + formValues.config[testKey].useCustomJudge === 'true') && + + } + />} + {testIndex === 0 && +
+ + } + > + + +
} + +
+
+ ); +}; + +EditExerciseSimpleConfigTest.propTypes = { + testName: PropTypes.string.isRequired, + test: PropTypes.string.isRequired, + testId: PropTypes.number.isRequired, + testKey: PropTypes.string.isRequired, + testIndex: PropTypes.number.isRequired, + supplementaryFiles: PropTypes.array.isRequired, + exerciseTests: PropTypes.array, + formValues: PropTypes.object, + testErrors: PropTypes.object, + smartFill: PropTypes.func.isRequired, + intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired +}; + +export default injectIntl(EditExerciseSimpleConfigTest); diff --git a/src/components/forms/EditExerciseSimpleConfigForm/index.js b/src/components/forms/EditExerciseSimpleConfigForm/index.js new file mode 100644 index 000000000..2ca500c0a --- /dev/null +++ b/src/components/forms/EditExerciseSimpleConfigForm/index.js @@ -0,0 +1 @@ +export default from './EditExerciseSimpleConfigForm'; diff --git a/src/components/forms/EditPipelineForm/EditPipelineForm.js b/src/components/forms/EditPipelineForm/EditPipelineForm.js index d63160188..2029eed7d 100644 --- a/src/components/forms/EditPipelineForm/EditPipelineForm.js +++ b/src/components/forms/EditPipelineForm/EditPipelineForm.js @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { canUseDOM } from 'exenv'; import { connect } from 'react-redux'; import { reduxForm, Field, touch, formValueSelector } from 'redux-form'; import { FormattedMessage } from 'react-intl'; @@ -21,10 +20,6 @@ import { extractVariables } from '../../../helpers/boxes'; import { fetchSupplementaryFilesForPipeline } from '../../../redux/modules/pipelineFiles'; import { createGetPipelineFiles } from '../../../redux/selectors/pipelineFiles'; -if (canUseDOM) { - require('codemirror/mode/yaml/yaml'); -} - class EditPipelineForm extends Component { componentDidMount = () => this.props.loadAsync(); componentWillReceiveProps = props => { diff --git a/src/components/forms/EditScoreConfigForm/EditScoreConfigForm.js b/src/components/forms/EditScoreConfigForm/EditScoreConfigForm.js index bf5aa2cde..7dade812d 100644 --- a/src/components/forms/EditScoreConfigForm/EditScoreConfigForm.js +++ b/src/components/forms/EditScoreConfigForm/EditScoreConfigForm.js @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { canUseDOM } from 'exenv'; import { reduxForm, Field } from 'redux-form'; import { FormattedMessage } from 'react-intl'; import { Alert } from 'react-bootstrap'; @@ -8,10 +7,6 @@ import { Alert } from 'react-bootstrap'; import { SourceCodeField } from '../Fields'; import SubmitButton from '../SubmitButton'; -if (canUseDOM) { - require('codemirror/mode/yaml/yaml'); -} - const EditScoreConfigForm = ({ anyTouched, submitting, 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/index.js b/src/components/forms/EditSimpleLimits/index.js deleted file mode 100644 index ece1d0922..000000000 --- a/src/components/forms/EditSimpleLimits/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './EditSimpleLimits'; diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js new file mode 100644 index 000000000..c560f8fe9 --- /dev/null +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -0,0 +1,248 @@ +import React from 'react'; +import { Alert, Table } from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +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 { + encodeTestId, + encodeEnvironmentId +} from '../../../redux/modules/simpleLimits'; +import prettyMs from 'pretty-ms'; + +import styles from './styles.less'; + +const EditSimpleLimitsForm = ({ + environments, + tests, + cloneHorizontally, + cloneVertically, + cloneAll, + reset, + handleSubmit, + anyTouched, + dirty, + submitting, + submitFailed, + submitSucceeded, + invalid +}) => + + } + unlimitedHeight + noPadding + success={submitSucceeded} + dirty={dirty} + footer={ +
+ {dirty && + + {' '} + } + + ), + submitting: ( + + ), + success: ( + + ), + validating: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + + + + + )} + + + + {tests.map(test => + + + + {environments.map(environment => { + const id = + encodeTestId(test.id) + + '.' + + encodeEnvironmentId(environment.id); + return ( + + ); + })} + + )} + +
+ {environments.map(environment => + + {environment.name} +
+ {test.name} + 1 ? styles.colSeparator : ''} + > + +
+
; + +EditSimpleLimitsForm.propTypes = { + tests: PropTypes.array.isRequired, + environments: PropTypes.array, + cloneHorizontally: PropTypes.func.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + anyTouched: PropTypes.bool, + dirty: PropTypes.bool, + submitting: PropTypes.bool, + submitFailed: PropTypes.bool, + submitSucceeded: PropTypes.bool, + invalid: PropTypes.bool +}; + +const validate = ({ limits }) => { + const errors = {}; + const maxSumTime = 300; // 5 minutes + + // 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; + } + } + }) + ); + + // 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: false, + immutableProps: [ + 'environments', + 'tests', + 'cloneHorizontally', + 'cloneVertically', + 'cloneAll', + 'handleSubmit' + ], + validate +})(EditSimpleLimitsForm); diff --git a/src/components/forms/EditSimpleLimitsForm/index.js b/src/components/forms/EditSimpleLimitsForm/index.js new file mode 100644 index 000000000..46652bfe3 --- /dev/null +++ b/src/components/forms/EditSimpleLimitsForm/index.js @@ -0,0 +1 @@ +export default from './EditSimpleLimitsForm'; diff --git a/src/components/forms/EditSimpleLimits/styles.less b/src/components/forms/EditSimpleLimitsForm/styles.less similarity index 52% rename from src/components/forms/EditSimpleLimits/styles.less rename to src/components/forms/EditSimpleLimitsForm/styles.less index 84a884ca6..eaa1dff46 100644 --- a/src/components/forms/EditSimpleLimits/styles.less +++ b/src/components/forms/EditSimpleLimitsForm/styles.less @@ -8,3 +8,12 @@ padding: 20px; margin: 10px; } + +.colSeparator { + border-left: 1px solid #ddd; +} + +.limitsTableHeading { + font-size: 130%; + white-space: nowrap; +} diff --git a/src/components/forms/EditTestsForm/EditTests.css b/src/components/forms/EditTestsForm/EditTests.css new file mode 100644 index 000000000..3a64352fa --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTests.css @@ -0,0 +1,4 @@ +.testRow { + margin-bottom: 0; + margin-top: 0; +} diff --git a/src/components/forms/EditTestsForm/EditTestsForm.js b/src/components/forms/EditTestsForm/EditTestsForm.js new file mode 100644 index 000000000..5f2135cc7 --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTestsForm.js @@ -0,0 +1,181 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { reduxForm, FieldArray, Field, getFormValues } from 'redux-form'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { Alert } from 'react-bootstrap'; + +import EditTestsTest from './EditTestsTest'; +import { CheckboxField } from '../Fields'; +import SubmitButton from '../SubmitButton'; +import Button from '../../widgets/FlatButton'; +import { RefreshIcon } from '../../icons'; + +class EditTestsForm extends Component { + render() { + const { + dirty, + submitting, + handleSubmit, + reset, + hasFailed = false, + hasSucceeded = false, + invalid, + formValues + } = this.props; + + return ( +
+ {hasFailed && + + + } + + + } + /> + + + +
+ {dirty && + !submitting && + !hasSucceeded && + + {' '} + } + + + ), + submitting: ( + + ), + success: ( + + ) + }} + /> +
+
+ ); + } +} + +EditTestsForm.propTypes = { + values: PropTypes.array, + reset: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + dirty: PropTypes.bool, + submitting: PropTypes.bool, + hasFailed: PropTypes.bool, + hasSucceeded: PropTypes.bool, + invalid: PropTypes.bool, + formValues: PropTypes.object +}; + +const validate = ({ isUniform, tests }) => { + const errors = {}; + + const testsErrors = {}; + const knownTests = new Set(); + for (let i = 0; i < tests.length; ++i) { + const test = tests[i]; + const testErrors = {}; + if (!test.name || test.name === '') { + testErrors['name'] = ( + + ); + } + if (knownTests.has(test.name)) { + testErrors['name'] = ( + + ); + } + knownTests.add(test.name); + if (!isUniform && (!test.weight || test.weight === '')) { + testErrors['weight'] = ( + + ); + } + const weight = Number.parseInt(test.weight); + if (!isUniform && (!Number.isFinite(weight) || weight < 0)) { + testErrors['weight'] = ( + + ); + } + testsErrors[i] = testErrors; + } + errors['tests'] = testsErrors; + + return errors; +}; + +export default connect(state => { + return { + formValues: getFormValues('editTests')(state) + }; +})( + reduxForm({ + form: 'editTests', + enableReinitialize: true, + keepDirtyOnReinitialize: false, + validate + })(EditTestsForm) +); diff --git a/src/components/forms/EditTestsForm/EditTestsTest.js b/src/components/forms/EditTestsForm/EditTestsTest.js new file mode 100644 index 000000000..cb7c048ec --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTestsTest.js @@ -0,0 +1,86 @@ +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, testValues }) => { + const weightSum = isUniform + ? fields.length + : testValues.reduce((acc, val) => acc + Number(val.weight), 0); + + return ( +
+ + + + + {!isUniform && + } + + + + + {fields.map((test, index) => + fields.remove(index)} + /> + )} + +
+ + + + + + +
+ {fields.length < 99 && +
+ +
} +
+ ); +}; + +EditTestsTest.propTypes = { + fields: PropTypes.object.isRequired, + isUniform: PropTypes.bool.isRequired, + testValues: PropTypes.array.isRequired +}; + +export default formValues({ + testValues: 'tests' +})(EditTestsTest); diff --git a/src/components/forms/EditTestsForm/EditTestsTestRow.js b/src/components/forms/EditTestsForm/EditTestsTestRow.js new file mode 100644 index 000000000..809e5a43d --- /dev/null +++ b/src/components/forms/EditTestsForm/EditTestsTestRow.js @@ -0,0 +1,56 @@ +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, percent }) => + + + + + {!isUniform && + + + } + + {percent} + + + + + ; + +EditTestsTestRow.propTypes = { + test: PropTypes.string.isRequired, + onRemove: PropTypes.func.isRequired, + isUniform: PropTypes.bool.isRequired, + percent: PropTypes.string.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/components/forms/EditUserProfileForm/EditUserProfileForm.js b/src/components/forms/EditUserProfileForm/EditUserProfileForm.js index f3d6efd70..5f495f725 100644 --- a/src/components/forms/EditUserProfileForm/EditUserProfileForm.js +++ b/src/components/forms/EditUserProfileForm/EditUserProfileForm.js @@ -5,7 +5,7 @@ import { reduxForm, Field, change } from 'redux-form'; import { Alert } from 'react-bootstrap'; import FormBox from '../../widgets/FormBox'; import SubmitButton from '../SubmitButton'; -import { Throttle } from 'react-throttle'; + import { validateRegistrationData } from '../../../redux/modules/users'; import { TextField, PasswordField, PasswordStrength } from '../Fields'; @@ -167,21 +167,18 @@ const EditUserProfileForm = ({ } /> - - asyncValidate()} - label={ - - } - /> - + + } + /> { @@ -17,7 +17,9 @@ const CheckboxField = ({ /* eslint-disable no-unneeded-ternary */ return ( @@ -25,12 +27,12 @@ const CheckboxField = ({ {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } ); @@ -41,7 +43,11 @@ CheckboxField.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) }).isRequired, - meta: PropTypes.object.isRequired, + meta: PropTypes.shape({ + dirty: PropTypes.bool, + error: PropTypes.oneOfType([PropTypes.string, FormattedMessage]), + warning: PropTypes.oneOfType([PropTypes.string, FormattedMessage]) + }).isRequired, type: PropTypes.string, onOff: PropTypes.bool, label: PropTypes.oneOfType([ diff --git a/src/components/forms/Fields/DatetimeField.js b/src/components/forms/Fields/DatetimeField.js index dc373d836..1bdba31d2 100644 --- a/src/components/forms/Fields/DatetimeField.js +++ b/src/components/forms/Fields/DatetimeField.js @@ -5,9 +5,12 @@ import Datetime from 'react-datetime'; import 'react-datetime/css/react-datetime.css'; import { FormGroup, ControlLabel, HelpBlock } from 'react-bootstrap'; +import classNames from 'classnames'; import withLinks from '../../../hoc/withLinks'; +import styles from './commonStyles.less'; + class DatetimeField extends Component { /** * This hack forces redux-form to open the calendar each time @@ -24,9 +27,10 @@ class DatetimeField extends Component { render() { const { input, - meta: { touched, error }, + meta: { active, dirty, error, warning }, disabled, label, + ignoreDirty = false, ...props } = this.props; @@ -35,7 +39,7 @@ class DatetimeField extends Component { return ( {label} this.onFocus()} inputProps={{ disabled }} + bsClass={classNames({ + 'form-control': true, + [styles.dirty]: dirty && !ignoreDirty && !error && !warning, + [styles.active]: active + })} />{' '} {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } ); @@ -75,10 +84,13 @@ DatetimeField.propTypes = { ]) }).isRequired, meta: PropTypes.shape({ - touched: PropTypes.bool, - error: PropTypes.any + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any }).isRequired, - disabled: PropTypes.bool + disabled: PropTypes.bool, + ignoreDirty: PropTypes.bool }; export default withLinks(DatetimeField); diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js new file mode 100644 index 000000000..99d86b61a --- /dev/null +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -0,0 +1,290 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field } from 'redux-form'; +import { FormattedMessage } from 'react-intl'; +import { Row, Col, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; + +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 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, prettyPrintBytesWrap); +const validateTime = validateValue(limitRanges.time, prettyPrintMsWrap); + +const EditSimpleLimitsField = ({ + prefix, + id, + testsCount, + environmentsCount, + cloneVertically, + cloneHorizontally, + cloneAll, + ...props +}) => +
+ + = 3 ? 12 : 6} md={12}> + + + + } + validate={validateMemory} + {...props} + /> + + + + +
+ {testsCount > 1 && + + + + } + > + + + + } + {environmentsCount > 1 && + + } + > + + + + } + > + + + + + } + {testsCount > 1 && + environmentsCount > 1 && + + } + > + + + + } + > + + + + + } +
+ + = 3 ? 12 : 6} md={12}> + + + + } + {...props} + /> + + + + +
+ {testsCount > 1 && + + + + } + > + + + + } + {environmentsCount > 1 && + + } + > + + + + } + > + + + + + } + {testsCount > 1 && + environmentsCount > 1 && + + } + > + + + + } + > + + + + + } +
+ +
+
; + +EditSimpleLimitsField.propTypes = { + id: PropTypes.string.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneHorizontally: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, + prefix: PropTypes.string.isRequired, + testsCount: PropTypes.number.isRequired, + environmentsCount: PropTypes.number.isRequired +}; + +export default EditSimpleLimitsField; diff --git a/src/components/forms/Fields/EditSimpleLimitsField.less b/src/components/forms/Fields/EditSimpleLimitsField.less new file mode 100644 index 000000000..025ea7747 --- /dev/null +++ b/src/components/forms/Fields/EditSimpleLimitsField.less @@ -0,0 +1,8 @@ +.buttonsCol { + white-space: nowrap; + text-align: center; + color: #999; + padding-left: 10px; + padding-bottom: 0; + vertical-align: top; +} diff --git a/src/components/forms/Fields/ExpandingInputFilesField.js b/src/components/forms/Fields/ExpandingInputFilesField.js new file mode 100644 index 000000000..711dd881e --- /dev/null +++ b/src/components/forms/Fields/ExpandingInputFilesField.js @@ -0,0 +1,141 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Field } from 'redux-form'; +import { ControlLabel, OverlayTrigger, Tooltip } from 'react-bootstrap'; + +import Icon from 'react-fontawesome'; + +import FlatButton from '../../widgets/FlatButton'; + +import SelectField from './SelectField'; +import TextField from './TextField'; + +import styles from './commonStyles.less'; + +const EMPTY_VALUE = { file: '', name: '' }; + +const validate = value => + !value || value.trim() === '' + ? + : undefined; + +const ExpandingInputFilesField = ({ + fields, + meta: { active, dirty, error, warning }, + leftLabel = '', + rightLabel = '', + options, + ...props +}) => +
+ {fields.length > 0 && + + + + + + + + + {fields.map((field, index) => + + + + + + )} + +
+ + {leftLabel} + + + + {rightLabel} + + +
+ + + + + + + + } + > + fields.remove(index)}> + + + +
} +
+ {fields.length === 0 && + + + } + + + + } + > + fields.push(EMPTY_VALUE)}> + + + +
+
; + +ExpandingInputFilesField.propTypes = { + fields: PropTypes.object.isRequired, + meta: PropTypes.shape({ + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any + }).isRequired, + leftLabel: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) + ]).isRequired, + rightLabel: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) + ]).isRequired, + options: PropTypes.array +}; + +export default ExpandingInputFilesField; diff --git a/src/components/forms/Fields/ExpandingSelectField.js b/src/components/forms/Fields/ExpandingSelectField.js index cabf21e58..bdc8cc16f 100644 --- a/src/components/forms/Fields/ExpandingSelectField.js +++ b/src/components/forms/Fields/ExpandingSelectField.js @@ -8,6 +8,9 @@ import { HelpBlock, ControlLabel } from 'react-bootstrap'; +import classNames from 'classnames'; + +import styles from './commonStyles.less'; class ExpandingSelectField extends Component { state = { texts: [''] }; @@ -44,10 +47,11 @@ class ExpandingSelectField extends Component { render() { const { label = '', - input: { name, onChange }, - meta: { touched, error }, + input: { name, onChange, onFocus, onBlur }, + meta: { active, dirty, error, warning }, options, style = {}, + ignoreDirty = false, ...props } = this.props; const { texts } = this.state; @@ -55,7 +59,7 @@ class ExpandingSelectField extends Component { return ( {label}
@@ -63,9 +67,23 @@ class ExpandingSelectField extends Component { this.changeText(i, e.target.value, onChange)} - onBlur={() => this.removeIfEmpty(i, onChange)} + onFocus={onFocus} + onBlur={e => { + onBlur(e); + this.removeIfEmpty(i, onChange); + }} value={text} componentClass="select" + bsClass={classNames({ + 'form-control': true, + [styles.dirty]: + i < texts.length - 1 && + dirty && + !ignoreDirty && + !error && + !warning, + [styles.active]: active + })} {...props} > {options.map(({ key, name }, o) => @@ -78,12 +96,12 @@ class ExpandingSelectField extends Component {
{' '} {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} }
); @@ -92,13 +110,19 @@ class ExpandingSelectField extends Component { ExpandingSelectField.propTypes = { input: PropTypes.object, - meta: PropTypes.object, + meta: PropTypes.shape({ + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any + }).isRequired, options: PropTypes.array, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) ]).isRequired, - style: PropTypes.object + style: PropTypes.object, + ignoreDirty: PropTypes.bool }; export default ExpandingSelectField; diff --git a/src/components/forms/Fields/ExpandingTextField.js b/src/components/forms/Fields/ExpandingTextField.js index ac836f7d6..859b0be0c 100644 --- a/src/components/forms/Fields/ExpandingTextField.js +++ b/src/components/forms/Fields/ExpandingTextField.js @@ -1,100 +1,109 @@ -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, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; -import { - FormGroup, - FormControl, - HelpBlock, - ControlLabel -} from 'react-bootstrap'; +import FlatButton from '../../widgets/FlatButton'; -class ExpandingTextField extends Component { - state = { texts: [''] }; +import TextField from './TextField'; - componentDidMount() { - const { input: { value } } = this.props; - const initialValue = Array.isArray(value) - ? value.concat(['']) - : [value, '']; - this.setState({ texts: initialValue }); - } +import styles from './commonStyles.less'; - 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); - }; - - 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 }, - meta: { touched, error }, - style = {}, - ...props - } = this.props; - const { texts } = this.state; - - return ( - - {label} -
- {texts.map((text, i) => - this.changeText(i, e.target.value, onChange)} - onBlur={() => this.removeIfEmpty(i, onChange)} - value={text} - {...props} +const ExpandingTextField = ({ + fields, + meta: { active, dirty, error, warning }, + label, + ...props +}) => +
+ + {label} + + + + {fields.map((field, index) => + + + + + + )} + +
+ + + + + + } + > + fields.insert(index, '')}> + + + + + + + + } + > + fields.remove(index)}> + + + +
+
+ {fields.length === 0 && + + + } + + - )} -
{' '} - {error && - - {' '}{touched - ? error - : }{' '} - } - - ); - } -} + + } + > + fields.push('')}> + + + +
+
; ExpandingTextField.propTypes = { - input: PropTypes.object, - meta: PropTypes.object, + fields: PropTypes.object.isRequired, + meta: PropTypes.shape({ + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any + }).isRequired, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) - ]).isRequired, - style: PropTypes.object + ]).isRequired }; export default ExpandingTextField; diff --git a/src/components/forms/Fields/KiloBytesTextField.js b/src/components/forms/Fields/KiloBytesTextField.js index e3a7bb9e9..d1d0f6195 100644 --- a/src/components/forms/Fields/KiloBytesTextField.js +++ b/src/components/forms/Fields/KiloBytesTextField.js @@ -1,11 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import prettyBytes from 'pretty-bytes'; -import TextField from './TextField'; import { HelpBlock } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; -const KiloBytesTextField = ({ input, ...props }) => ( +import { prettyPrintBytes } from '../../helpers/stringFormatters'; +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 }) =>
@@ -13,10 +15,9 @@ const KiloBytesTextField = ({ input, ...props }) => ( id="app.bytesTextField.humanReadable" defaultMessage="Human readable variant:" />{' '} - {prettyBytes(Number(input.value) * 1000)} + {prettyPrintBytes(Number(input.value) * 1024)} -
-); + ; KiloBytesTextField.propTypes = { input: PropTypes.shape({ diff --git a/src/components/forms/Fields/LimitsField.js b/src/components/forms/Fields/LimitsField.js index ba495f21c..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, @@ -16,7 +17,7 @@ const LimitsField = ({ setVertically, setAll, ...props -}) => ( +}) =>

-
-); + ; LimitsField.propTypes = { label: PropTypes.oneOfType([ 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 }) => + + + + + + + {prettyPrint(input.value)} + + + ; + +LimitsValueField.propTypes = { + input: PropTypes.shape({ + value: PropTypes.any.isRequired + }).isRequired, + prettyPrint: PropTypes.func.isRequired +}; + +export default LimitsValueField; diff --git a/src/components/forms/Fields/PipelineVariablesField.js b/src/components/forms/Fields/PipelineVariablesField.js index 5ba6c26d9..dd21f1551 100644 --- a/src/components/forms/Fields/PipelineVariablesField.js +++ b/src/components/forms/Fields/PipelineVariablesField.js @@ -11,13 +11,13 @@ import { } from '../Fields'; import ResourceRenderer from '../../helpers/ResourceRenderer'; -const isArray = (firstValue, type = '') => - firstValue.length > 0 && - firstValue[0] !== '$' && +const isArray = (firstVal, type = '') => + firstVal.length > 0 && + firstVal[0] !== '$' && typeof type === 'string' && type.indexOf('[]') === type.length - 2; -const firstValue = value => (Array.isArray(value) ? value[0] : value); +const firstValue = value => (Array.isArray(value) ? value[0] || '' : value); const PipelineVariablesField = ({ input, @@ -50,7 +50,11 @@ const PipelineVariablesField = ({ {(...supplementaryFiles) => a.name.localeCompare(b.name, intl.locale)) diff --git a/src/components/forms/Fields/PortField.js b/src/components/forms/Fields/PortField.js index 4e221b6f7..6ebf21442 100644 --- a/src/components/forms/Fields/PortField.js +++ b/src/components/forms/Fields/PortField.js @@ -9,21 +9,25 @@ import { HelpBlock, Label } from 'react-bootstrap'; +import classNames from 'classnames'; import { isArrayType } from '../../../helpers/boxes'; +import styles from './commonStyles.less'; + const getLabelStyle = portType => (isArrayType(portType) ? 'primary' : 'info'); const PortField = ({ input, - meta: { touched, error }, + meta: { active, dirty, error, warning }, label, portType, + ignoreDirty = false, ...props }) => {label}{' '} @@ -34,15 +38,24 @@ const PortField = ({ {portType} - + {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } ; @@ -51,14 +64,17 @@ PortField.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired }).isRequired, meta: PropTypes.shape({ - touched: PropTypes.bool, - error: PropTypes.any + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any }).isRequired, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) ]).isRequired, - portType: PropTypes.string.isRequired + portType: PropTypes.string.isRequired, + ignoreDirty: PropTypes.bool }; export default PortField; diff --git a/src/components/forms/Fields/RichTextAreaField.js b/src/components/forms/Fields/RichTextAreaField.js deleted file mode 100644 index 8733be71e..000000000 --- a/src/components/forms/Fields/RichTextAreaField.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import RichTextEditor, { createEmptyValue } from 'react-rte'; -import { Checkbox, FormGroup } from 'react-bootstrap'; -import TextAreaField from './TextAreaField'; - -class RichTextAreaField extends Component { - componentWillMount = () => { - const { viewSource = false } = this.props; - this.setState({ - viewSource, - value: createEmptyValue() - }); - }; - - componentWillReceiveProps = newProps => { - const { viewSource, value } = this.state; - const inputVal = newProps.input.value; - const sourceEditedDirectly = - viewSource && inputVal !== this.props.input.value; - const formWasReset = - !viewSource && - inputVal !== value.toString('markdown') && - inputVal.length === 0; - - if (sourceEditedDirectly || formWasReset) { - this.setState({ - value: this.state.value.setContentFromString(inputVal, 'markdown') - }); - } - }; - - toggleViewSource = e => { - this.setState({ viewSource: !this.state.viewSource }); - }; - - onChange = value => { - const { onChange, input } = this.props; - const markdown = value.toString('markdown'); - this.setState({ value }); - input.onChange(markdown); - onChange && onChange(markdown); - }; - - render() { - const { disabled = false } = this.props; - const { viewSource, value } = this.state; - return ( -
- {viewSource && } - {!viewSource && - this.onChange(e)} - readonly={disabled} - useDefaultStyles={false} - />} - - this.toggleViewSource()} - > - - - -
- ); - } -} - -RichTextAreaField.propTypes = { - ...TextAreaField.propTypes, - input: PropTypes.shape({ - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired - }).isRequired, - viewSource: PropTypes.string -}; - -export default RichTextAreaField; diff --git a/src/components/forms/Fields/SecondsTextField.js b/src/components/forms/Fields/SecondsTextField.js index 65c892dd5..a033c045f 100644 --- a/src/components/forms/Fields/SecondsTextField.js +++ b/src/components/forms/Fields/SecondsTextField.js @@ -5,21 +5,21 @@ import TextField from './TextField'; import { HelpBlock } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; -const SecondsTextField = ({ input, ...props }) => ( +// !!! 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 }) =>
{!props.meta.error && - !isNaN(Number(input.value)) && ( - - {' '} - {prettyMs(Number(input.value) * 1000)} - - )} -
-); + !isNaN(Number(input.value)) && + false && + + {' '} + {prettyMs(Number(input.value) * 1000)} + } + ; SecondsTextField.propTypes = { input: PropTypes.shape({ diff --git a/src/components/forms/Fields/SelectField.js b/src/components/forms/Fields/SelectField.js index 4b56e79a0..a2724dd35 100644 --- a/src/components/forms/Fields/SelectField.js +++ b/src/components/forms/Fields/SelectField.js @@ -8,24 +8,38 @@ import { ControlLabel, HelpBlock } from 'react-bootstrap'; +import classNames from 'classnames'; + +import styles from './commonStyles.less'; const SelectField = ({ input, - meta: { touched, error }, + meta: { active, dirty, warning, error }, label, options, addEmptyOption = false, emptyOptionCaption = '...', + ignoreDirty = false, ...props }) => - - {label} - - + {label && + + {label} + } + {addEmptyOption && {error && - {touched - ? error - : } + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } ; @@ -51,7 +65,12 @@ SelectField.propTypes = { input: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired, - meta: PropTypes.shape({ error: PropTypes.any, touched: PropTypes.bool }), + meta: PropTypes.shape({ + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any + }).isRequired, type: PropTypes.string, label: PropTypes.oneOfType([ PropTypes.string, @@ -59,7 +78,8 @@ SelectField.propTypes = { ]).isRequired, options: PropTypes.array.isRequired, addEmptyOption: PropTypes.bool, - emptyOptionCaption: PropTypes.string + emptyOptionCaption: PropTypes.string, + ignoreDirty: PropTypes.bool }; export default SelectField; diff --git a/src/components/forms/Fields/TextAreaField.js b/src/components/forms/Fields/TextAreaField.js index 617a0ff51..836464f95 100644 --- a/src/components/forms/Fields/TextAreaField.js +++ b/src/components/forms/Fields/TextAreaField.js @@ -1,38 +1,51 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; - import { FormGroup, FormControl, ControlLabel, HelpBlock } from 'react-bootstrap'; +import classNames from 'classnames'; + +import styles from './commonStyles.less'; const TextAreaField = ({ input, - meta: { touched, error }, + meta: { active, dirty, error, warning }, type = 'text', label, children, + ignoreDirty = false, ...props }) => {label} - + {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } {children} ; @@ -48,9 +61,12 @@ TextAreaField.propTypes = { ]).isRequired, children: PropTypes.any, meta: PropTypes.shape({ - touched: PropTypes.bool, - error: PropTypes.oneOfType([PropTypes.string, FormattedMessage]) - }) + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any + }).isRequired, + ignoreDirty: PropTypes.bool }; export default TextAreaField; diff --git a/src/components/forms/Fields/TextField.js b/src/components/forms/Fields/TextField.js index 63737ecc1..d47009001 100644 --- a/src/components/forms/Fields/TextField.js +++ b/src/components/forms/Fields/TextField.js @@ -1,28 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; - import { FormGroup, FormControl, ControlLabel, HelpBlock } from 'react-bootstrap'; +import classNames from 'classnames'; + +import styles from './commonStyles.less'; const TextField = ({ input: { value, ...input }, - meta: { touched, error }, + meta: { active, dirty, error, warning }, type = 'text', label, + groupClassName = '', + ignoreDirty = false, ...props }) => - - {label} - + {label && + + {label} + } {error && - {' '}{touched - ? error - : }{' '} + {' '}{error}{' '} + } + {!error && + warning && + + {' '}{warning}{' '} } ; @@ -54,13 +65,17 @@ TextField.propTypes = { ]).isRequired }).isRequired, meta: PropTypes.shape({ - touched: PropTypes.bool, - error: PropTypes.any + active: PropTypes.bool, + dirty: PropTypes.bool, + error: PropTypes.any, + warning: PropTypes.any }).isRequired, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }) - ]).isRequired + ]).isRequired, + groupClassName: PropTypes.string, + ignoreDirty: PropTypes.bool }; export default TextField; diff --git a/src/components/forms/Fields/commonStyles.less b/src/components/forms/Fields/commonStyles.less new file mode 100644 index 000000000..c475810ed --- /dev/null +++ b/src/components/forms/Fields/commonStyles.less @@ -0,0 +1,11 @@ +.dirty { + box-shadow: inset 0 0 12px #bf9; +} + +.active, .active * { + box-shadow: 0 0 10px #bcb!important; +} + +.alignTop { + vertical-align: top; +} diff --git a/src/components/forms/Fields/index.js b/src/components/forms/Fields/index.js index 8ea92ad06..4981a83c6 100644 --- a/src/components/forms/Fields/index.js +++ b/src/components/forms/Fields/index.js @@ -2,6 +2,8 @@ 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 LimitsValueField } from './LimitsValueField'; export { default as LimitsField } from './LimitsField'; export { default as PasswordField } from './PasswordField'; export { default as PasswordStrength } from './PasswordStrength'; @@ -20,6 +22,9 @@ export { default as TextAreaField } from './TextAreaField'; export { default as TextField } from './TextField'; export { default as ExpandingTextField } from './ExpandingTextField'; export { default as ExpandingSelectField } from './ExpandingSelectField'; +export { + default as ExpandingInputFilesField +} from './ExpandingInputFilesField'; export { default as CASAuthenticationButtonField } from './CASAuthenticationButtonField'; diff --git a/src/components/forms/OnOffCheckbox/OnOffCheckbox.js b/src/components/forms/OnOffCheckbox/OnOffCheckbox.js index 8bdbd6b2c..3c57f3da5 100644 --- a/src/components/forms/OnOffCheckbox/OnOffCheckbox.js +++ b/src/components/forms/OnOffCheckbox/OnOffCheckbox.js @@ -16,7 +16,7 @@ const OnOffCheckbox = ({ disabled, checked, ...props -}) => ( +}) => {children} - -); + ; OnOffCheckbox.propTypes = { checked: PropTypes.bool, diff --git a/src/components/forms/RegistrationForm/RegistrationForm.js b/src/components/forms/RegistrationForm/RegistrationForm.js index b411c83b8..ffdd9be7e 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, @@ -16,7 +17,6 @@ import { } from '../Fields'; import { validateRegistrationData } from '../../../redux/modules/users'; import SubmitButton from '../SubmitButton'; -import { Throttle } from 'react-throttle'; const RegistrationForm = ({ submitting, @@ -111,19 +111,18 @@ const RegistrationForm = ({ } /> - - asyncValidate()} - label={ - - } - /> - + + eventAggregator('RegistrationFormAsyncValidate', asyncValidate, 500)} + label={ + + } + /> = base && unit < units.length) { + absValue /= base; + ++unit; + } + const rounded = Math.round(absValue * 1000) / 1000; + return { value: rounded.toString(), unit: units[unit] }; +} + +export function prettyPrintBytes(input) { + const { value, unit } = parseBytes(input); + return `${value} ${unit}`; +} + +export function prettyPrintPercent(percent) { + percent = Number(percent); + if (Number.isNaN(percent)) { + return '-'; + } + return (Math.round(percent * 1000) / 10).toString() + '%'; +} diff --git a/src/components/widgets/SourceCodeInfoBox/SourceCodeInfoBox.js b/src/components/widgets/SourceCodeInfoBox/SourceCodeInfoBox.js index c209f8b47..803ca5f96 100644 --- a/src/components/widgets/SourceCodeInfoBox/SourceCodeInfoBox.js +++ b/src/components/widgets/SourceCodeInfoBox/SourceCodeInfoBox.js @@ -1,15 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { SimpleInfoBox } from '../InfoBox'; -import prettyBytes from 'pretty-bytes'; +import { prettyPrintBytes } from '../../helpers/stringFormatters'; -const SourceCodeInfoBox = ({ id, name, size, uploadedAt }) => ( +const SourceCodeInfoBox = ({ id, name, size, uploadedAt }) => -); + />; SourceCodeInfoBox.propTypes = { id: PropTypes.string.isRequired, diff --git a/src/containers/EvaluationProgressContainer/EvaluationProgressContainer.js b/src/containers/EvaluationProgressContainer/EvaluationProgressContainer.js index 73abc9240..2d25af3da 100644 --- a/src/containers/EvaluationProgressContainer/EvaluationProgressContainer.js +++ b/src/containers/EvaluationProgressContainer/EvaluationProgressContainer.js @@ -147,9 +147,10 @@ class EvaluationProgressContainer extends Component { }; finish = () => { - const { push, link, finishProcessing } = this.props; + const { push, link, finishProcessing, onFinish } = this.props; finishProcessing(); this.closeSocket(); + onFinish && onFinish(); push(link); }; @@ -234,7 +235,8 @@ EvaluationProgressContainer.propTypes = { messages: PropTypes.object, intl: PropTypes.object.isRequired, push: PropTypes.func.isRequired, - onUserClose: PropTypes.func + onUserClose: PropTypes.func, + onFinish: PropTypes.func }; export default connect( diff --git a/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js b/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js index ef78c712f..f69b0806d 100644 --- a/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js +++ b/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js @@ -3,7 +3,10 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ResubmitSolution from '../../components/buttons/ResubmitSolution'; -import { resubmitSubmission } from '../../redux/modules/submissions'; +import { + resubmitSubmission, + fetchUsersSubmissions +} from '../../redux/modules/submissions'; import { isProcessing, getMonitorParams, @@ -11,14 +14,17 @@ import { } from '../../redux/selectors/submission'; import EvaluationProgressContainer from '../EvaluationProgressContainer'; import withLinks from '../../hoc/withLinks'; +import { fetchSubmissionEvaluationsForSolution } from '../../redux/modules/submissionEvaluations'; const ResubmitSolutionContainer = ({ id, assignmentId, + userId, resubmit, monitor, isProcessing, newSubmissionId, + fetchSubmissions, isDebug = true, links: { SUBMISSION_DETAIL_URI_FACTORY } }) => { @@ -29,6 +35,7 @@ const ResubmitSolutionContainer = ({ isOpen={isProcessing} monitor={monitor} link={SUBMISSION_DETAIL_URI_FACTORY(assignmentId, newSubmissionId)} + onFinish={() => fetchSubmissions(userId)} /> ); @@ -43,7 +50,9 @@ ResubmitSolutionContainer.propTypes = { isProcessing: PropTypes.bool, newSubmissionId: PropTypes.string, links: PropTypes.object.isRequired, - isDebug: PropTypes.bool + isDebug: PropTypes.bool, + userId: PropTypes.string, + fetchSubmissions: PropTypes.func }; const mapStateToProps = state => ({ @@ -52,8 +61,16 @@ const mapStateToProps = state => ({ newSubmissionId: getSubmissionId(state) }); -const mapDispatchToProps = (dispatch, { id, isPrivate = false }) => ({ - resubmit: isDebug => dispatch(resubmitSubmission(id, isPrivate, isDebug)) +const mapDispatchToProps = ( + dispatch, + { id, isPrivate = false, assignmentId } +) => ({ + resubmit: isDebug => dispatch(resubmitSubmission(id, isPrivate, isDebug)), + fetchSubmissions: userId => + Promise.all([ + dispatch(fetchSubmissionEvaluationsForSolution(id)), + dispatch(fetchUsersSubmissions(userId, assignmentId)) + ]) }); export default withLinks( diff --git a/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js b/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js index cad0ecb81..1fd98a0c8 100644 --- a/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js +++ b/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js @@ -101,6 +101,7 @@ class SubmitSolutionContainer extends Component { monitor={monitor} link={SUBMISSION_DETAIL_URI_FACTORY(id, submissionId)} onUserClose={fetchSubmissions} + onFinish={fetchSubmissions} />} ); diff --git a/src/helpers/eventAggregator.js b/src/helpers/eventAggregator.js new file mode 100644 index 000000000..ee8a20a83 --- /dev/null +++ b/src/helpers/eventAggregator.js @@ -0,0 +1,34 @@ +var pending = {}; + +export function eventAggregator(id, fnc, timeout) { + if (typeof fnc !== 'function') { + throw new Error('Argument fnc has to be a function.'); + } + + if (pending[id]) { + // Previous event with the same ID was registered, timeout pending... + pending[id].fnc = fnc; + } else { + // First event of given ID, lets start countdown ... + const tid = setTimeout(() => { + if (pending[id] && pending[id].fnc) { + pending[id].fnc(); + delete pending[id]; + } + }, timeout); + pending[id] = { + tid, + fnc + }; + } +} + +export function terminateAggregation(id, finalInvoke = false) { + if (pending[id]) { + clearTimeout(pending[id].tid); + if (finalInvoke && pending[id].fnc) { + pending[id].fnc(); + } + delete pending[id]; + } +} diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js new file mode 100644 index 000000000..23783d8cf --- /dev/null +++ b/src/helpers/exerciseSimpleForm.js @@ -0,0 +1,369 @@ +import yaml from 'js-yaml'; +import { + endpointDisguisedAsIdFactory, + encodeTestId, + encodeEnvironmentId +} from '../redux/modules/simpleLimits'; + +export const getEnvInitValues = environmentConfigs => { + let res = {}; + for (const env of environmentConfigs) { + res[env.runtimeEnvironmentId] = true; + } + return res; +}; + +export const transformAndSendEnvValues = ( + formData, + environments, + editEnvironmentConfigs, + reloadConfigAndLimits +) => { + let res = []; + for (const env in formData) { + if (formData[env] !== true && formData[env] !== 'true') { + continue; + } + let envObj = { runtimeEnvironmentId: env }; + const currentFullEnv = environments.find(e => e.id === env); + envObj.variablesTable = currentFullEnv.defaultVariables; + res.push(envObj); + } + return editEnvironmentConfigs({ environmentConfigs: res }).then( + reloadConfigAndLimits + ); +}; + +export const getTestsInitValues = (exerciseTests, scoreConfig, locale) => { + const jsonScoreConfig = yaml.safeLoad(scoreConfig); + const testWeights = jsonScoreConfig.testWeights || {}; + const sortedTests = exerciseTests.sort((a, b) => + a.name.localeCompare(b.name, locale) + ); + + let res = []; + let allWeightsSame = true; + let lastWeight = null; + for (const test of sortedTests) { + const testWeight = testWeights[test.name] || 100; + if (lastWeight !== null && testWeight !== lastWeight) { + allWeightsSame = false; + } + lastWeight = testWeight; + res.push({ id: test.id, name: test.name, weight: String(testWeight) }); + } + + return { isUniform: allWeightsSame, tests: res }; +}; + +export const transformAndSendTestsValues = ( + formData, + editExerciseTests, + editExerciseScoreConfig +) => { + const uniformScore = + formData.isUniform === true || formData.isUniform === 'true'; + let scoreConfigData = { testWeights: {} }; + let testsData = []; + + for (const test of formData.tests) { + const testWeight = uniformScore ? 100 : Number(test.weight); + scoreConfigData.testWeights[test.name] = testWeight; + + testsData.push( + test.id ? { id: test.id, name: test.name } : { name: test.name } + ); + } + + return Promise.all([ + editExerciseTests({ tests: testsData }), + editExerciseScoreConfig({ scoreConfig: yaml.safeDump(scoreConfigData) }) + ]); +}; + +export const getSimpleConfigInitValues = (config, tests, locale) => { + const confTests = + tests && config[0] && config[0].tests ? config[0].tests : []; + + 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' + ); + const actualInputs = variables.find( + variable => variable.name === 'actual-inputs' + ); + if (inputFiles) { + testObj.inputFiles = inputFiles.value + ? inputFiles.value.map((value, i) => ({ + file: value, + name: + actualInputs && actualInputs.value && actualInputs.value[i] + ? actualInputs.value[i].trim() + : '' + })) + : []; + } + + const expectedOutput = variables.find( + variable => variable.name === 'expected-output' + ); + if (expectedOutput) { + testObj.expectedOutput = expectedOutput.value; + } + + const runArgs = variables.find(variable => variable.name === 'run-args'); + 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 && actualOutput.value && actualOutput.value.trim()) { + testObj.useOutFile = true; + testObj.outputFile = actualOutput.value.trim(); + } else { + testObj.useOutFile = false; + testObj.outputFile = ''; + } + + const stdinFile = variables.find( + variable => variable.name === 'stdin-file' + ); + if (stdinFile) { + testObj.inputStdin = stdinFile.value; + } + + const standardJudge = variables.find( + variable => variable.name === 'judge-type' + ); + const customJudge = variables.find( + variable => variable.name === 'custom-judge' + ); + + testObj.useCustomJudge = false; + testObj.customJudgeBinary = ''; + testObj.judgeBinary = ''; + if (customJudge && customJudge.value) { + testObj.customJudgeBinary = customJudge.value; + testObj.useCustomJudge = true; + } + if (!testObj.useCustomJudge) { + testObj.judgeBinary = + standardJudge && standardJudge.value + ? standardJudge.value + : 'recodex-judge-normal'; + } + + const judgeArgs = variables.find( + variable => variable.name === 'judge-args' + ); + testObj.judgeArgs = []; + if (judgeArgs && judgeArgs.value) { + testObj.judgeArgs = Array.isArray(judgeArgs.value) + ? judgeArgs.value + : [judgeArgs.value]; + } + + res[encodeTestId(test.id)] = testObj; + } + + return { config: res }; +}; + +export const transformAndSendConfigValues = ( + formData, + pipelines, + environments, + tests, + setConfig +) => { + let testVars = []; + for (let t of tests) { + const testName = t.id; + const test = formData.config[encodeTestId(testName)]; + let variables = []; + + variables.push({ + name: 'custom-judge', + type: 'remote-file', + value: test.useCustomJudge ? test.customJudgeBinary : '' + }); + variables.push({ + name: 'expected-output', + type: 'remote-file', + value: test.expectedOutput + }); + variables.push({ + name: 'judge-type', + type: 'string', + value: test.judgeBinary + }); + variables.push({ + name: 'stdin-file', + type: 'remote-file', + value: test.inputStdin + }); + variables.push({ + name: 'judge-args', + type: 'string[]', + value: test.judgeArgs + }); + variables.push({ + name: 'run-args', + type: 'string[]', + value: test.runArgs + }); + if (test.useOutFile) { + variables.push({ + name: 'actual-output', + type: 'file', + value: test.useOutFile ? test.outputFile.trim() : '' + }); + } + + let inputFiles = []; + let renamedNames = []; + const inFilesArr = + test.inputFiles && Array.isArray(test.inputFiles) ? test.inputFiles : []; + for (const item of inFilesArr) { + inputFiles.push(item.file); + renamedNames.push(item.name.trim()); + } + variables.push({ + name: 'input-files', + type: 'remote-file[]', + value: inputFiles + }); + variables.push({ + name: 'actual-inputs', + type: 'file[]', + value: renamedNames + }); + + testVars.push({ + name: testName, + variables: variables, + producesFiles: test.useOutFile + }); + } + + let envs = []; + for (const environment of environments) { + const envId = environment.runtimeEnvironmentId; + const envPipelines = pipelines.filter( + pipeline => pipeline.runtimeEnvironmentIds.indexOf(envId) >= 0 + ); + + let testsCfg = []; + for (const testVar of testVars) { + const compilationPipelineId = envPipelines.filter( + pipeline => pipeline.parameters.isCompilationPipeline + )[0].id; + const executionPipelineId = envPipelines.filter( + pipeline => + pipeline.parameters.isExecutionPipeline && + (testVar.producesFiles + ? pipeline.parameters.producesFiles + : pipeline.parameters.producesStdout) + )[0].id; + testsCfg.push({ + name: testVar.name, + pipelines: [ + { + name: compilationPipelineId, + variables: [] + }, + { + name: executionPipelineId, + variables: testVar.variables + } + ] + }); + } + envs.push({ + name: envId, + tests: testsCfg + }); + } + + 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, + environments, + exerciseId +) => { + let res = {}; + + tests.forEach(test => { + const testEnc = encodeTestId(test.id); + res[testEnc] = {}; + environments.forEach(environment => { + const envId = encodeEnvironmentId(environment.id); + let lim = limits.getIn([ + endpointDisguisedAsIdFactory({ + exerciseId, + runtimeEnvironmentId: environment.id + }), + 'data', + String(test.id) + ]); + if (lim) { + lim = lim.toJS(); + } + + res[testEnc][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.id] = formData.limits[encodeTestId(test.id)][envId]; + return acc; + }, {}) + }; + return editEnvironmentSimpleLimits(environment.id, data); + }) + ); 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/locales/cs.json b/src/locales/cs.json index d4a5d5092..dd9956100 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", @@ -12,14 +17,14 @@ "app.addLicence.validation.note": "Popis nemůže být prázdný.", "app.addLicence.validation.validUntilEmpty": "Konec platnosti licence musí být nastaven.", "app.addLicence.validation.validUntilInThePast": "Platnost licence musí být v budoucnosti.", - "app.attachmentFilesTable.description": "Přídavné soubory k úloze jsou soubory, které mohou být použity v popisu úlohy pomocí unikátních odkazů zobrazených níže. Tyto soubory mohou být zobrazovány a stahovány studenty.", - "app.attachmentFilesTable.title": "Přídavné soubory k úloze", - "app.attachmentFiles.deleteButton": "Delete", - "app.attachmentFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", - "app.attachmentFilesTable.fileName": "Původní název", - "app.attachmentFilesTable.fileSize": "Velikost", - "app.attachmentFilesTable.fileUploadedAt": "Nahráno", - "app.attachmentFilesTable.url": "URL", + "app.additionalExerciseFilesTable.description": "Additional exercise files are files which can be used within exercise description using links provided below. Additional files can be viewed or downloaded by students.", + "app.additionalExerciseFilesTable.title": "Additional exercise files", + "app.additionalFiles.deleteButton": "Delete", + "app.additionalFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.additionalFilesTable.fileName": "Original filename", + "app.additionalFilesTable.fileSize": "Filesize", + "app.additionalFilesTable.fileUploadedAt": "Uploaded at", + "app.additionalFilesTable.url": "URL", "app.adminAssignments.actions": "Akce", "app.adminAssignments.deadline": "Termín odevzdání", "app.adminAssignments.name": "Název zadání", @@ -48,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", @@ -66,9 +73,17 @@ "app.assignments.secondDeadline": "Druhý termín odevzdání", "app.assignmentsTable.noAssignments": "Nenalezeny žádné zadané úlohy.", "app.assignmentsTableRow.loading": "Načítají se zadané úlohy ...", - "app.filesTable.addFiles": "Uložit soubory", - "app.filesTable.empty": "Zatím zde nejsou žádné nahrané soubory.", - "app.filesTable.title": "Přiložené soubory", + "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": "Nepodařilo se načíst data", "app.badge.failedLoadingInfo": "Prosim zkontrolujte si své připojení k Internetu.", "app.badge.loading": "Načítání ...", @@ -122,20 +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": "Popis:", - "app.createGroup.groupName": "Jméno:", "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.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.", "app.createGroup.validation.thresholdMustBeInteger": "Procentuální hranice musí být celé číslo.", - "app.createGroupForm.createGroup": "Vytvořit novou skupinu", - "app.createGroupForm.failed": "Vytváření nové skupiny selhalo. Za vzniklé nepříjemnosti se omlouváme.", - "app.createGroupForm.processing": "Skupina je právě vytvářena ...", - "app.createGroupForm.success": "Skupina byla vytvořena", - "app.createGroupForm.title": "Vytvořit novou skupinu", "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", @@ -219,12 +225,21 @@ "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": "Uložení se nezdařilo. Prosíme opakujte akci později.", + "app.editEnvironmentSimpleForm.reset": "Obnovit původní", + "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": "Nastavení konfigurace úlohy", "app.editExercise.editEnvironmentConfig": "Upravit konfigurace prostředí", + "app.editExercise.editEnvironments": "Nastavení běhových prostředí", "app.editExercise.editScoreConfig": "Edit score configurations", - "app.editExercise.editTestConfig": "Upravit konfigurace", + "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", @@ -236,9 +251,12 @@ "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.", + "app.editExerciseConfigForm.validation.duplicateInputFile": "Duplicate name detected. The input file aliases must be unique.", "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é", @@ -254,7 +272,7 @@ "app.editExerciseForm.submitting": "Ukládání změn ...", "app.editExerciseForm.success": "Nastavení bylo uloženo.", "app.editExerciseForm.title": "Upravit nastavení úlohy", - "app.editExerciseForm.validating": "Validování...", + "app.editExerciseForm.validating": "Validuji ...", "app.editExerciseForm.validation.description": "Prosíme vyplňte interní popis úlohy.", "app.editExerciseForm.validation.difficulty": "Prosíme vyberte obtížnost úlohy.", "app.editExerciseForm.validation.emptyName": "Prosíme vyplňte název úlohy.", @@ -264,6 +282,31 @@ "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.editExerciseSimpleConfig.noTests": "There are no tests yet. The form cannot be displayed until at least one test is created.", + "app.editExerciseSimpleConfig.noTestsOrEnvironments": "There are no tests or no enabled environments yet. The form cannot be displayed until at least one test is created and one environment is enabled.", + "app.editExerciseSimpleConfigForm.reset": "Obnovit původní", + "app.editExerciseSimpleConfigForm.submit": "Uložit konfiguraci", + "app.editExerciseSimpleConfigForm.submitting": "Ukládám konfiguraci ...", + "app.editExerciseSimpleConfigForm.success": "Konfigurace uložena", + "app.editExerciseSimpleConfigForm.validating": "Validuji ...", + "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.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í.", @@ -324,6 +367,31 @@ "app.editScoreConfigForm.submit": "Change configuration", "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", + "app.editSimpleLimitsField.tooltip.cloneAll": "Copy this value to all tests in all environments.", + "app.editSimpleLimitsField.tooltip.cloneHorizontal": "Copy this value horizontally to all environments of the test.", + "app.editSimpleLimitsField.tooltip.cloneVertical": "Copy this value vertically to all tests within the environment.", + "app.editSimpleLimitsForm.failed": "Nepodařilo se uložit limity. Prosím, opakujte akci později.", + "app.editSimpleLimitsForm.reset": "Obnovit původní", + "app.editSimpleLimitsForm.submit": "Uložit limity", + "app.editSimpleLimitsForm.submitting": "Ukládám limity ...", + "app.editSimpleLimitsForm.success": "Limity uloženy.", + "app.editSimpleLimitsForm.validating": "Validuji ...", + "app.editSimpleLimitsForm.validation.timeSum": "Součet časových limitů ({sum}) překračuje povolené maximum ({max}).", + "app.editTestsForm.failed": "Ukládání selhalo. Prosím, opakujte akci později.", + "app.editTestsForm.isUniform": "Použít rovnoměrné rozdělení bodů mezi testy.", + "app.editTestsForm.reset": "Obnovit původní", + "app.editTestsForm.submit": "Uložit testy", + "app.editTestsForm.submitting": "Ukládám testy ...", + "app.editTestsForm.success": "Testy uloženy.", + "app.editTestsForm.validation.testName": "Prosím, vyplňte jméno testu.", + "app.editTestsForm.validation.testNameTaken": "Zvolené jméno již existuje, prosím zadejte jiné.", + "app.editTestsForm.validation.testWeight": "Váha testu musí být kladné celé číslo.", + "app.editTestsForm.validation.testWeightEmpty": "Prosím, vyplňte váhu testu.", + "app.editTestsTest.add": "Přidat test", + "app.editTestsTest.name": "Název testu:", + "app.editTestsTest.pointsPercentage": "Body v procentech:", + "app.editTestsTest.remove": "Odebrat", + "app.editTestsTest.weight": "Váha testu:", "app.editUser.description": "Upravit nastavení uživatele", "app.editUser.title": "Upravit uživatelský profil", "app.editUserProfile.degreesAfterName": "Tituly za jménem:", @@ -480,6 +548,14 @@ "app.exitCodes.mono.201": "No main method", "app.exitCodes.mono.202": "More main methods", "app.exitCodes.unknown": "Unknown", + "app.expandingInputFilesField.noFiles": "There are no files yet...", + "app.expandingInputFilesField.tooltip.add": "Add another input file.", + "app.expandingInputFilesField.tooltip.remove": "Remove this file from input files.", + "app.expandingInputFilesField.validateEmpty": "This value must not be empty.", + "app.expandingTextField.noItems": "There are no items yet...", + "app.expandingTextField.tooltip.add": "Append a new item.", + "app.expandingTextField.tooltip.addAbove": "Insert new item right above.", + "app.expandingTextField.tooltip.remove": "Remove this item from the list.", "app.externalRegistrationForm.failed": "Registrace se nezdařila. Prosíme, zkontrolujte vyplněné informace.", "app.externalRegistrationForm.instance": "Instance:", "app.externalRegistrationForm.password": "Heslo:", @@ -501,20 +577,14 @@ "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": "Test memory limit:", - "app.fields.limits.time": "Test time limit:", + "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.forkExerciseButton.confirmation": "Opravdu chcete zduplokovat tuto úlohu?", - "app.forkExerciseButton.failed": "Zkuste zduplikovat tuto úlohu znovu", - "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", @@ -876,7 +946,7 @@ "app.submistSolution.instructions": "Musíte připojit alespoň jeden soubor se zdrojovým kódem a počkat, než jsou všechny soubory nahrány na server. Pokud nastane problém při uploadu některých soborů, nahrajte je znovu nebo soubory odeberte. Jméno souboru NESMÍ OBSAHOVAT žádné nestandardní znaky (například v kódování UTF-8).", "app.submistSolution.submitFailed": "Odevzdané řešení bylo aktivně odmítnuto. Toto je obvykle způsobeno nahráním souborů se jménem obsahujícím nestandardní znaky nebo špatnou koncovkou. Pokud nemůžete odevzdat řešení bez zjevného důvodu, kontaktujte prosím svého cvičícího.", "app.submitButton.invalid": "Nějaký vstup není validní", - "app.submitButton.validating": "Validování ...", + "app.submitButton.validating": "Validuji ...", "app.submitRefSolution.noteLabel": "Popis referenčního řešení", "app.submitRefSolution.submitButton": "Vytvořit nové referenční řešení", "app.submitRefSolution.submittingButtonText": "Vytváření nového referenčního řešení ...", @@ -895,8 +965,8 @@ "app.sudebar.menu.admin.title": "Administrátor", "app.sudebar.menu.student.title": "Student", "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.supplementaryFiles.deleteButton": "Smazat", + "app.supplementaryFiles.deleteConfirm": "Opravdu si přejete tento soubor smazat? Tato operace nemůže být vrácena.", "app.supplementaryFilesTable.description": "Soubory úlohy jsou soubory, které mohou být použity v nastavení úlohy jako vstupy nebo vzorové výstupy.", "app.supplementaryFilesTable.fileName": "Původní název", "app.supplementaryFilesTable.fileSize": "Velikost souboru", @@ -927,5 +997,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": "Sudí binárních dat", + "recodex-judge-float": "Sudí reálných čísel", + "recodex-judge-float-newline": "Sudí reálných čísel (ignorující konce řádků)", + "recodex-judge-normal": "Sudí tokenů", + "recodex-judge-normal-newline": "Sudí tokenů (ignorující konce řádků)", + "recodex-judge-shuffle": "Sudí neuspořádaných tokenů", + "recodex-judge-shuffle-all": "Sudí neuspořádaných tokenů a řádků", + "recodex-judge-shuffle-newline": "Sudí neuspořádaných tokenů (ignorující konce řádků)", + "recodex-judge-shuffle-rows": "Sudí neuspořádaných řádků" +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 4d1bad856..b05d29a5c 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", @@ -12,14 +17,14 @@ "app.addLicence.validation.note": "Note cannot be empty.", "app.addLicence.validation.validUntilEmpty": "End of licence's valid period must be set.", "app.addLicence.validation.validUntilInThePast": "End of licence's valid period must be in the future.", - "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.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.fileName": "Original filename", - "app.attachmentFilesTable.fileSize": "Filesize", - "app.attachmentFilesTable.fileUploadedAt": "Uploaded at", - "app.attachmentFilesTable.url": "URL", + "app.additionalExerciseFilesTable.description": "Additional exercise files are files which can be used within exercise description using links provided below. Additional files can be viewed or downloaded by students.", + "app.additionalExerciseFilesTable.title": "Additional exercise files", + "app.additionalFiles.deleteButton": "Delete", + "app.additionalFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.", + "app.additionalFilesTable.fileName": "Original filename", + "app.additionalFilesTable.fileSize": "Filesize", + "app.additionalFilesTable.fileUploadedAt": "Uploaded at", + "app.additionalFilesTable.url": "URL", "app.adminAssignments.actions": "Actions", "app.adminAssignments.deadline": "Deadline", "app.adminAssignments.name": "Assignment name", @@ -48,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", @@ -66,9 +73,17 @@ "app.assignments.secondDeadline": "Second deadline", "app.assignmentsTable.noAssignments": "There are no assignments.", "app.assignmentsTableRow.loading": "Loading assignments ...", - "app.filesTable.addFiles": "Save files", - "app.filesTable.empty": "There are no uploaded files yet.", - "app.filesTable.title": "Attached files", + "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 ...", @@ -122,20 +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.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.", "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", @@ -219,12 +225,21 @@ "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.reset": "Reset", + "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.", "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", @@ -236,9 +251,12 @@ "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.", + "app.editExerciseConfigForm.validation.duplicateInputFile": "Duplicate name detected.", "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", @@ -264,6 +282,31 @@ "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.editExerciseSimpleConfig.noTests": "There are no tests yet. The form cannot be displayed until at least one test is created.", + "app.editExerciseSimpleConfig.noTestsOrEnvironments": "There are no tests or no enabled environments yet. The form cannot be displayed until at least one test is created and one environment is enabled.", + "app.editExerciseSimpleConfigForm.reset": "Reset", + "app.editExerciseSimpleConfigForm.submit": "Save Configuration", + "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 in the expected output file.", + "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": "Rename as:", + "app.editExerciseSimpleConfigTests.inputStdin": "Stdin:", + "app.editExerciseSimpleConfigTests.inputTitle": "Input", + "app.editExerciseSimpleConfigTests.judgeArgs": "Judge arguments:", + "app.editExerciseSimpleConfigTests.judgeTitle": "Judge", + "app.editExerciseSimpleConfigTests.judgeType": "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 +367,31 @@ "app.editScoreConfigForm.submit": "Change configuration", "app.editScoreConfigForm.submitting": "Saving configuration ...", "app.editScoreConfigForm.success": "Configuration was changed.", + "app.editSimpleLimitsField.tooltip.cloneAll": "Copy this value to all tests in all environments.", + "app.editSimpleLimitsField.tooltip.cloneHorizontal": "Copy this value horizontally to all environments of the test.", + "app.editSimpleLimitsField.tooltip.cloneVertical": "Copy this value vertically to all tests within the environment.", + "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", + "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.reset": "Reset", + "app.editTestsForm.submit": "Save Tests", + "app.editTestsForm.submitting": "Saving Tests ...", + "app.editTestsForm.success": "Tests Saved.", + "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.pointsPercentage": "Points Percentage:", + "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:", @@ -480,6 +548,14 @@ "app.exitCodes.mono.201": "No main method", "app.exitCodes.mono.202": "More main methods", "app.exitCodes.unknown": "Unknown", + "app.expandingInputFilesField.noFiles": "There are no files yet...", + "app.expandingInputFilesField.tooltip.add": "Add another input file.", + "app.expandingInputFilesField.tooltip.remove": "Remove this file from input files.", + "app.expandingInputFilesField.validateEmpty": "This value must not be empty.", + "app.expandingTextField.noItems": "There are no items yet...", + "app.expandingTextField.tooltip.add": "Append a new item.", + "app.expandingTextField.tooltip.addAbove": "Insert new item right above.", + "app.expandingTextField.tooltip.remove": "Remove this item from the list.", "app.externalRegistrationForm.failed": "Registration failed. Please check your information.", "app.externalRegistrationForm.instance": "Instance:", "app.externalRegistrationForm.password": "Password:", @@ -502,19 +578,13 @@ "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 [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.forkExerciseButton.confirmation": "Do you really want to fork this exercise?", - "app.forkExerciseButton.failed": "Try forking the exercise again", - "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", @@ -927,5 +997,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": "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/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 373e8b19a..71b2f3a4d 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, @@ -94,12 +90,9 @@ class EditExerciseConfig extends Component { exerciseConfig, exerciseEnvironmentConfig, exerciseScoreConfig, - editEnvironmentSimpleLimits, + // editEnvironmentSimpleLimits, pipelines, - limits, - setHorizontally, - setVertically, - setAll, + // limits, 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 new file mode 100644 index 000000000..0c537c2fb --- /dev/null +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -0,0 +1,458 @@ +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 EditSimpleLimitsForm from '../../components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm'; +import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer'; +import EditTestsForm from '../../components/forms/EditTestsForm'; +import EditExerciseSimpleConfigForm from '../../components/forms/EditExerciseSimpleConfigForm'; +import EditEnvironmentSimpleForm from '../../components/forms/EditEnvironmentSimpleForm'; + +import { + fetchExercise, + fetchExerciseIfNeeded +} from '../../redux/modules/exercises'; +import { + fetchExerciseEnvironmentSimpleLimitsIfNeeded, + editEnvironmentSimpleLimits, + cloneHorizontally, + cloneVertically, + cloneAll, + fetchExerciseEnvironmentSimpleLimits +} from '../../redux/modules/simpleLimits'; +import { + fetchExerciseConfig, + fetchExerciseConfigIfNeeded, + setExerciseConfig +} 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'; +import { exerciseEnvironmentConfigSelector } from '../../redux/selectors/exerciseEnvironmentConfigs'; +import { + fetchExerciseEnvironmentConfig, + fetchExerciseEnvironmentConfigIfNeeded, + setExerciseEnvironmentConfig +} from '../../redux/modules/exerciseEnvironmentConfigs'; +import { exerciseScoreConfigSelector } from '../../redux/selectors/exerciseScoreConfig'; +import { + fetchScoreConfigIfNeeded, + setScoreConfig +} from '../../redux/modules/exerciseScoreConfig'; +import { + fetchExerciseTestsIfNeeded, + setExerciseTests +} from '../../redux/modules/exerciseTests'; +import { exerciseTestsSelector } from '../../redux/selectors/exerciseTests'; +import { fetchPipelines } from '../../redux/modules/pipelines'; +import { pipelinesSelector } from '../../redux/selectors/pipelines'; + +import { + getEnvInitValues, + transformAndSendEnvValues, + getTestsInitValues, + transformAndSendTestsValues, + getSimpleConfigInitValues, + transformAndSendConfigValues, + getLimitsInitValues, + transformAndSendLimitsValues +} from '../../helpers/exerciseSimpleForm'; + +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(fetchExerciseEnvironmentConfigIfNeeded(exerciseId)), + dispatch(fetchScoreConfigIfNeeded(exerciseId)), + dispatch(fetchExerciseTestsIfNeeded(exerciseId)), + dispatch(fetchRuntimeEnvironments()), + dispatch(fetchPipelines()) + ]); + + render() { + const { + links: { EXERCISE_URI_FACTORY }, + params: { exerciseId }, + exercise, + runtimeEnvironments, + exerciseConfig, + fetchEnvironmentSimpleLimits, + editEnvironmentSimpleLimits, + exerciseEnvironmentConfig, + editEnvironmentConfigs, + exerciseScoreConfig, + exerciseTests, + editScoreConfig, + editTests, + fetchConfig, + setConfig, + limits, + pipelines, + cloneHorizontally, + cloneVertically, + cloneAll, + reloadConfigAndLimits, + 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 + > + + {(scoreConfig, tests) => + + transformAndSendTestsValues( + data, + editTests, + editScoreConfig + ).then(() => + Promise.all([ + fetchConfig(), + fetchEnvironmentSimpleLimits() + ]) + )} + />} + + + + } + unlimitedHeight + > + + {(environmentConfigs, ...environments) => + + transformAndSendEnvValues( + data, + environments, + editEnvironmentConfigs, + reloadConfigAndLimits(exercise.id) + )} + />} + + + + + + + +
+ + + + + {(config, tests, environments, ...pipelines) => { + const sortedTests = tests.sort((a, b) => + a.name.localeCompare(b.name, locale) + ); + return tests.length > 0 + ? + transformAndSendConfigValues( + data, + pipelines, + environments, + sortedTests, + setConfig + )} + /> + :
+

+ {' '} + +

+ +
; + }} +
+ +
+
+ + + + + {(tests, envConfig) => + tests.length > 0 && exercise.runtimeEnvironments.length > 0 + ? + transformAndSendLimitsValues( + data, + tests, + exercise.runtimeEnvironments, + editEnvironmentSimpleLimits + )} + environments={exercise.runtimeEnvironments} + tests={tests.sort((a, b) => + a.name.localeCompare(b.name, locale) + )} + initialValues={getLimitsInitValues( + limits, + tests, + exercise.runtimeEnvironments, + exercise.id + )} + cloneVertically={cloneVertically} + cloneHorizontally={cloneHorizontally} + cloneAll={cloneAll} + /> + :
+

+ {' '} + +

+ +
} +
+ +
+
} +
+ ); + } +} + +EditExerciseSimpleConfig.propTypes = { + exercise: ImmutablePropTypes.map, + runtimeEnvironments: PropTypes.object.isRequired, + loadAsync: PropTypes.func.isRequired, + params: PropTypes.shape({ + exerciseId: PropTypes.string.isRequired + }).isRequired, + exerciseConfig: PropTypes.object, + exerciseEnvironmentConfig: PropTypes.object, + editEnvironmentConfigs: PropTypes.func.isRequired, + fetchEnvironmentSimpleLimits: PropTypes.func.isRequired, + editEnvironmentSimpleLimits: PropTypes.func.isRequired, + exerciseScoreConfig: PropTypes.object, + exerciseTests: PropTypes.object, + editScoreConfig: PropTypes.func.isRequired, + editTests: PropTypes.func.isRequired, + fetchConfig: PropTypes.func.isRequired, + setConfig: PropTypes.func.isRequired, + links: PropTypes.object.isRequired, + limits: PropTypes.object.isRequired, + pipelines: ImmutablePropTypes.map, + cloneHorizontally: PropTypes.func.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, + reloadConfigAndLimits: 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: simpleLimitsSelector(state), + exerciseEnvironmentConfig: exerciseEnvironmentConfigSelector( + exerciseId + )(state), + exerciseScoreConfig: exerciseScoreConfigSelector(exerciseId)(state), + exerciseTests: exerciseTestsSelector(exerciseId)(state), + pipelines: pipelinesSelector(state) + }; + }, + (dispatch, { params: { exerciseId } }) => ({ + loadAsync: () => + EditExerciseSimpleConfig.loadAsync({ exerciseId }, dispatch), + fetchEnvironmentSimpleLimits: () => + dispatch( + fetchExerciseIfNeeded(exerciseId) + ).then(({ value: exercise }) => + Promise.all( + exercise.runtimeEnvironments.map(environment => + dispatch( + fetchExerciseEnvironmentSimpleLimits( + exerciseId, + environment.id + ) + ) + ) + ) + ), + editEnvironmentSimpleLimits: (runtimeEnvironmentId, data) => + dispatch( + editEnvironmentSimpleLimits(exerciseId, runtimeEnvironmentId, data) + ), + editEnvironmentConfigs: data => + dispatch(setExerciseEnvironmentConfig(exerciseId, data)), + editScoreConfig: data => dispatch(setScoreConfig(exerciseId, data)), + editTests: data => dispatch(setExerciseTests(exerciseId, data)), + fetchConfig: () => dispatch(fetchExerciseConfig(exerciseId)), + setConfig: data => dispatch(setExerciseConfig(exerciseId, data)), + cloneVertically: ( + formName, + testName, + runtimeEnvironmentId + ) => field => () => + dispatch( + cloneVertically(formName, testName, runtimeEnvironmentId, field) + ), + cloneHorizontally: ( + formName, + testName, + runtimeEnvironmentId + ) => field => () => + dispatch( + cloneHorizontally(formName, testName, runtimeEnvironmentId, field) + ), + 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) + ) +); 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 {