diff --git a/public/public/term_cyr.ttf b/public/public/term_cyr.ttf deleted file mode 100644 index 8b9f399dc..000000000 Binary files a/public/public/term_cyr.ttf and /dev/null differ diff --git a/src/components/Submissions/TestResultsTable/TestResultsTable.js b/src/components/Submissions/TestResultsTable/TestResultsTable.js index 2ca8965fe..191bbab5d 100644 --- a/src/components/Submissions/TestResultsTable/TestResultsTable.js +++ b/src/components/Submissions/TestResultsTable/TestResultsTable.js @@ -12,7 +12,7 @@ import exitCodeMapping from '../../helpers/exitCodeMapping'; const hasValue = value => value !== null; const tickOrCrossAndRatioOrValue = (isOK, ratio, value, pretty, multiplier) => - {hasValue(value) && ') '} {hasValue(value) && pretty(value * multiplier)} - ; + ; + +const showTimeResults = ( + key, + wallTime, + wallTimeRatio, + wallTimeExceeded, + cpuTime, + cpuTimeRatio, + cpuTimeExceeded +) => { + const showWall = + Boolean(wallTimeRatio) || (wallTimeExceeded && !cpuTimeExceeded); + const showCpu = Boolean(cpuTimeRatio) || cpuTimeExceeded || !showWall; + return ( + + + {showCpu && + + + + } + {showWall && + + + + } + +
+ + + + } + > + + + + {tickOrCrossAndRatioOrValue( + cpuTimeExceeded === false, + cpuTimeRatio, + cpuTime, + prettyMs, + 1000 + )} +
+ + + + } + > + + + + {tickOrCrossAndRatioOrValue( + wallTimeExceeded === false, + wallTimeRatio, + wallTime, + prettyMs, + 1000 + )} +
+ ); +}; const TestResultsTable = ({ results, runtimeEnvironmentId }) => @@ -85,7 +159,7 @@ const TestResultsTable = ({ results, runtimeEnvironmentId }) => >    - + @@ -133,11 +207,14 @@ const TestResultsTable = ({ results, runtimeEnvironmentId }) => status, memoryExceeded, wallTimeExceeded, + cpuTimeExceeded, message, - wallTimeRatio, memoryRatio, - wallTime, + wallTimeRatio, + cpuTimeRatio, memory, + wallTime, + cpuTime, exitCode }) => @@ -181,20 +258,26 @@ const TestResultsTable = ({ results, runtimeEnvironmentId }) => - {tickOrCrossAndRatioOrValue( - memoryExceeded === false, - memoryRatio, - memory, - prettyPrintBytes, - 1024 - )} - {tickOrCrossAndRatioOrValue( - wallTimeExceeded === false, - wallTimeRatio, - wallTime, - prettyMs, - 1000 - )} + + +
+ + } + /> +
+ + +
+

+ + +

+
+ + +
+ {tickOrCrossAndRatioOrValue( + memoryExceeded === false, + memoryRatio, + memory, + prettyPrintBytes, + 1024 + )} + + {showTimeResults( + testName, + wallTime, + wallTimeRatio, + wallTimeExceeded, + cpuTime, + cpuTimeRatio, + cpuTimeExceeded + )} + {exitCodeMapping(runtimeEnvironmentId, exitCode)} diff --git a/src/components/forms/EditExerciseConfigForm/EditExerciseConfigForm.js b/src/components/forms/EditExerciseConfigForm/EditExerciseConfigForm.js index ab10768ce..5aa3b6a09 100644 --- a/src/components/forms/EditExerciseConfigForm/EditExerciseConfigForm.js +++ b/src/components/forms/EditExerciseConfigForm/EditExerciseConfigForm.js @@ -11,7 +11,7 @@ import Box from '../../../components/widgets/Box'; import SubmitButton from '../SubmitButton'; import EditExerciseConfigEnvironment from './EditExerciseConfigEnvironment'; import { fetchSupplementaryFilesForExercise } from '../../../redux/modules/supplementaryFiles'; -import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; +import { getSupplementaryFilesForExercise } from '../../../redux/selectors/supplementaryFiles'; import { getVariablesForPipelines } from '../../../redux/modules/exercises'; class EditExerciseConfigForm extends Component { @@ -327,11 +327,8 @@ const validate = ({ config }) => { const ConnectedEditExerciseConfigForm = connect( (state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); return { - supplementaryFiles: getSupplementaryFilesForExercise(state), + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(state), formValues: getFormValues('editExerciseConfig')(state) }; }, diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 2bfa740ad..7d64449d0 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { reduxForm, getFormValues } from 'redux-form'; @@ -13,122 +13,129 @@ import SubmitButton from '../SubmitButton'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import EditExerciseSimpleConfigTest from './EditExerciseSimpleConfigTest'; -import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; +import { getSupplementaryFilesForExercise } from '../../../redux/selectors/supplementaryFiles'; import { encodeTestId } from '../../../redux/modules/simpleLimits'; import { smartFillExerciseConfigForm } from '../../../redux/modules/exerciseConfigs'; import { exerciseConfigFormErrors } from '../../../redux/selectors/exerciseConfigs'; -const EditExerciseSimpleConfigForm = ({ - reset, - handleSubmit, - submitting, - submitFailed, - submitSucceeded, - invalid, - dirty, - formValues, - formErrors, - supplementaryFiles, - exerciseTests, - smartFill, - intl: { locale } -}) => - - } - unlimitedHeight - noPadding - success={submitSucceeded} - dirty={dirty} - footer={ -
- {dirty && - - {' '} - } +class EditExerciseSimpleConfigForm extends Component { + render() { + const { + reset, + handleSubmit, + submitting, + submitFailed, + submitSucceeded, + invalid, + dirty, + formValues, + formErrors, + supplementaryFiles, + exerciseTests, + smartFill, + intl: { locale } + } = this.props; - - ), - submitting: ( - - ), - success: ( - - ), - validating: ( - - ) - }} - /> -
- } - > - {submitFailed && - - - } - - {(...files) => -
- {exerciseTests - .sort((a, b) => a.name.localeCompare(b.name, locale)) - .map((test, idx) => - - )} -
} -
-
; + return ( + + } + unlimitedHeight + noPadding + success={submitSucceeded} + dirty={dirty} + footer={ +
+ {dirty && + + {' '} + } + + + ), + submitting: ( + + ), + success: ( + + ), + validating: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + {(...files) => +
+ {exerciseTests + .sort((a, b) => a.name.localeCompare(b.name, locale)) + .map((test, idx) => + + )} +
} +
+
+ ); + } +} EditExerciseSimpleConfigForm.propTypes = { initialValues: PropTypes.object, @@ -197,34 +204,29 @@ const validate = formData => { : undefined; }; -export default injectIntl( - 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) - ) +export default connect( + (state, { exercise }) => { + return { + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(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 + })(injectIntl(EditExerciseSimpleConfigForm)) ); diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index 4cb229df7..17db26c2a 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -1,15 +1,15 @@ -import React from 'react'; -import { Alert, Table } from 'react-bootstrap'; +import React, { Component } from 'react'; +import { Alert, Table, Row, Col } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { reduxForm } from 'redux-form'; +import { reduxForm, Field } from 'redux-form'; +import Icon from 'react-fontawesome'; -import { EditSimpleLimitsField } from '../Fields'; +import { EditSimpleLimitsField, CheckboxField } from '../Fields'; import SubmitButton from '../SubmitButton'; import FormBox from '../../widgets/FormBox'; import Button from '../../widgets/FlatButton'; import { RefreshIcon } from '../../icons'; - import { encodeTestId, encodeEnvironmentId @@ -18,156 +18,192 @@ 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, - intl: { locale } -}) => - - } - unlimitedHeight - noPadding - success={submitSucceeded} - dirty={dirty} - footer={ -
- {dirty && - - {' '} - } - - ), - submitting: ( - - ), - success: ( - - ), - validating: ( - - ) - }} - /> -
- } - > - {submitFailed && - - - } - - - - - - )} - - - - {tests.sort((a, b) => a.name.localeCompare(b.name, locale)).map(test => - - - - {environments.map(environment => { - const id = - encodeTestId(test.id) + - '.' + - encodeEnvironmentId(environment.id); - return ( - - ); - })} - - )} - -
- {environments.map(environment => - - {environment.name} -
- {test.name} - 1 ? styles.colSeparator : ''} +class EditSimpleLimitsForm extends Component { + render() { + const { + environments, + tests, + cloneHorizontally, + cloneVertically, + cloneAll, + reset, + handleSubmit, + dirty, + submitting, + submitFailed, + submitSucceeded, + invalid, + intl: { locale } + } = this.props; + return ( + + } + unlimitedHeight + noPadding + success={submitSucceeded} + dirty={dirty} + footer={ +
+ {dirty && + + {' '} + } + + ), + submitting: ( + -
-
; + ), + success: ( + + ), + validating: ( + + ) + }} + /> + + } + > + {submitFailed && + + + } + +
+ + + + )} + + + + {tests + .sort((a, b) => a.name.localeCompare(b.name, locale)) + .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, @@ -194,8 +230,8 @@ const validate = ({ limits }) => { 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 (limits[test][env]['time']) { + const val = Number(limits[test][env]['time']); if (!Number.isNaN(val) && val > 0) { sums[env] = (sums[env] || 0) + val; } @@ -210,7 +246,7 @@ const validate = ({ limits }) => { Object.keys(sums).forEach(env => { if (sums[env] > maxSumTime) { testsErrors[env] = { - 'wall-time': ( + time: ( { return errors; }; -export default injectIntl( - reduxForm({ - form: 'editSimpleLimits', - enableReinitialize: true, - keepDirtyOnReinitialize: false, - immutableProps: [ - 'environments', - 'tests', - 'cloneHorizontally', - 'cloneVertically', - 'cloneAll', - 'handleSubmit' - ], - validate - })(EditSimpleLimitsForm) -); +export default reduxForm({ + form: 'editSimpleLimits', + enableReinitialize: true, + keepDirtyOnReinitialize: false, + immutableProps: [ + 'environments', + 'tests', + 'cloneHorizontally', + 'cloneVertically', + 'cloneAll', + 'handleSubmit' + ], + validate +})(injectIntl(EditSimpleLimitsForm)); diff --git a/src/components/forms/EditSimpleLimitsForm/styles.less b/src/components/forms/EditSimpleLimitsForm/styles.less index eaa1dff46..c4fb1db6b 100644 --- a/src/components/forms/EditSimpleLimitsForm/styles.less +++ b/src/components/forms/EditSimpleLimitsForm/styles.less @@ -17,3 +17,15 @@ font-size: 130%; white-space: nowrap; } + +.preciseTime { + padding-top: 20px; + padding-left: 20px; + white-space: nowrap; +} + +.preciseTimeTooltip { + font-size: 80%; + padding: 2em; + color: #666; +} diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js index 99d86b61a..f4d41948e 100644 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ b/src/components/forms/Fields/EditSimpleLimitsField.js @@ -181,7 +181,7 @@ const EditSimpleLimitsField = ({ } > - + } {environmentsCount > 1 && 1 && environmentsCount > 1 && /> ; +const shallowResourcesEqual = (oldResources, newResources) => { + if (oldResources.length !== newResources.length) { + return false; + } + + // Ah, finally, some old-school for-loops... + for (let i = 0; i < oldResources.length; ++i) { + if (oldResources[i] !== newResources[i]) { + return false; + } + } + return true; +}; + /** * ResourceRenderer component is given a resource managed by the resourceManager * as a prop and displays different content based on the state of the given @@ -46,34 +60,62 @@ const defaultFailed = noIcons => * When all the files are fully loaded then the component displays the content * for the loaded state. */ -const ResourceRenderer = ({ - noIcons = false, - loading = defaultLoading(noIcons), - failed = defaultFailed(noIcons), - children: ready, - resource, - hiddenUntilReady = false, - forceLoading = false -}) => { - const resources = - List.isList(resource) || Array.isArray(resource) ? resource : [resource]; - const stillLoading = - !resource || - resources.find(res => !res) || - resources.some(isLoading) || - forceLoading; - return stillLoading - ? hiddenUntilReady ? null : loading - : resources.some(hasFailed) - ? hiddenUntilReady ? null : failed - : ready( - ...resources - .filter(res => !isDeleting(res)) - .filter(res => !isDeleted(res)) - .filter(res => !isPosting(res)) - .map(getJsData) - ); // display all ready items -}; +class ResourceRenderer extends Component { + componentWillMount = () => { + // Reset caching variables ... + this.oldResources = null; + this.oldData = null; + }; + + // Perform rendering of the childs whilst keeping resource data cached ... + renderReady = resources => { + const { children: ready, returnAsArray = false } = this.props; + if ( + this.oldResources === null || + !shallowResourcesEqual(this.oldResources, resources) + ) { + this.oldData = resources + .filter(res => !isDeleting(res)) + .filter(res => !isDeleted(res)) + .filter(res => !isPosting(res)) + .map( + (res, idx) => + // If a particular resource did not change, re-use its old data + this.oldResources && this.oldResources[idx] === res + ? this.oldData[idx] + : getJsData(res) + ); + this.oldResources = resources; + } + return returnAsArray ? ready(this.oldData) : ready(...this.oldData); + }; + + render() { + const { + noIcons = false, + loading = defaultLoading(noIcons), + failed = defaultFailed(noIcons), + resource, + hiddenUntilReady = false, + forceLoading = false + } = this.props; + + const resources = Array.isArray(resource) + ? resource + : List.isList(resource) ? resource.toArray() : [resource]; + const stillLoading = + !resource || + resources.find(res => !res) || + resources.some(isLoading) || + forceLoading; + + return stillLoading + ? hiddenUntilReady ? null : loading + : resources.some(hasFailed) + ? hiddenUntilReady ? null : failed + : this.renderReady(resources); + } +} ResourceRenderer.propTypes = { loading: PropTypes.element, @@ -86,7 +128,8 @@ ResourceRenderer.propTypes = { ]), hiddenUntilReady: PropTypes.bool, forceLoading: PropTypes.bool, - noIcons: PropTypes.bool + noIcons: PropTypes.bool, + returnAsArray: PropTypes.bool }; export default ResourceRenderer; diff --git a/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js b/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js index d6da76578..c88ca3d47 100644 --- a/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js +++ b/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js @@ -17,7 +17,7 @@ import { } from '../../redux/modules/supplementaryFiles'; import { downloadSupplementaryFile } from '../../redux/modules/files'; -import { createGetSupplementaryFiles } from '../../redux/selectors/supplementaryFiles'; +import { getSupplementaryFilesForExercise } from '../../redux/selectors/supplementaryFiles'; const SupplementaryFilesTableContainer = ({ exercise, @@ -69,11 +69,8 @@ SupplementaryFilesTableContainer.propTypes = { export default connect( (state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); return { - supplementaryFiles: getSupplementaryFilesForExercise(state) + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(state) }; }, (dispatch, { exercise }) => ({ diff --git a/src/helpers/debugTools.js b/src/helpers/debugTools.js new file mode 100644 index 000000000..3822e33ca --- /dev/null +++ b/src/helpers/debugTools.js @@ -0,0 +1,42 @@ +/* eslint eqeqeq: "off", no-console: ["error", { allow: ["log", "error", "debug"] }] */ +const modificationWatchdogValues = {}; + +export const modificationWatchdog = (name, value, verbose = false) => { + if (modificationWatchdogValues[name] === undefined) { + // First time the value is being registered ... + console.log(`Value '${name}' was created.`); + if (verbose) { + console.log(value); + } + } else if (modificationWatchdogValues[name] !== value) { + console.log(`Value '${name}' was changed.`); + if (verbose) { + console.log(modificationWatchdogValues[name]); + console.log(value); + } + } + modificationWatchdogValues[name] = value; + return value; +}; + +// This should be called from componentWillReceiveProps(nextProps), for instance. +export const logPropsChanges = (className, props, nextProps) => { + let changed = false; + for (const p in props) { + if (props[p] !== nextProps[p]) { + if (!changed) { + changed = true; + console.log(`${className} props changed.`); + } + + if (props[p] != nextProps[p]) { + console.log(`Property '${p}' changed.`); + console.log(props[p]); + console.log(nextProps[p]); + } else { + console.log(`Property '${p}' was recreated with the same value.`); + } + } + } + if (changed) console.log('----------'); +}; diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 6d7228ed0..7f256cd31 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -1,4 +1,6 @@ import yaml from 'js-yaml'; +import { defaultMemoize } from 'reselect'; + import { endpointDisguisedAsIdFactory, encodeTestId, @@ -37,23 +39,19 @@ export const getTestsInitValues = (exerciseTests, scoreConfig, locale) => { }; }; -export const transformAndSendTestsValues = ( - formData, - editExerciseTests, - editExerciseScoreConfig -) => { +export const transformTestsValues = formData => { const uniformScore = formData.isUniform === true || formData.isUniform === 'true'; let scoreConfigData = { testWeights: {} }; - let testsData = []; + let tests = []; for (const test of formData.tests) { const testWeight = uniformScore ? 100 : Number(test.weight); scoreConfigData.testWeights[test.name] = testWeight; - testsData.push( + tests.push( test.id ? { id: test.id, @@ -65,21 +63,22 @@ export const transformAndSendTestsValues = ( ); } - return Promise.all([ - editExerciseTests({ - tests: testsData - }), - editExerciseScoreConfig({ - scoreConfig: yaml.safeDump(scoreConfigData) - }) - ]); + return { + tests, + scoreConfig: yaml.safeDump(scoreConfigData) + }; }; /* * Environments */ -export const getEnvInitValues = environmentConfigs => { +export const getEnvInitValues = (environmentConfigs, environments) => { let res = {}; + // all environments + for (const env of environments) { + res[env.id] = false; // make sure we have all the environments set + } + // only environments in the config for (const env of environmentConfigs) { res[env.runtimeEnvironmentId] = true; } @@ -201,7 +200,7 @@ const getSimpleConfigSimpleVariable = (variables, testObj, variableName) => { }; // Prepare the initial form data for configuration form ... -export const getSimpleConfigInitValues = (config, tests) => { +export const getSimpleConfigInitValues = defaultMemoize((config, tests) => { const confTests = tests && config[0] && config[0].tests ? config[0].tests : []; @@ -247,7 +246,7 @@ export const getSimpleConfigInitValues = (config, tests) => { return { config: res }; -}; +}); // Prepare one variable to be sent in to the API const transformConfigSimpleVariable = (variables, name, value) => { @@ -321,13 +320,12 @@ const _safeGet = (obj, path) => { return obj; }; -export const transformAndSendConfigValues = ( +export const transformConfigValues = ( formData, pipelines, environments, tests, - originalConfig, - setConfig + originalConfig ) => { let envs = []; for (const environment of environments) { @@ -340,6 +338,7 @@ export const transformAndSendConfigValues = ( const compilationPipeline = envPipelines.find( pipeline => pipeline.parameters.isCompilationPipeline ); + const executionPipelineStdout = envPipelines.find( pipeline => pipeline.parameters.isExecutionPipeline && @@ -403,51 +402,72 @@ export const transformAndSendConfigValues = ( }); } - return setConfig({ - config: envs - }); + return { config: envs }; }; /* * Memory and Time limits */ -export const getLimitsInitValues = ( - limits, - tests, - environments, - exerciseId -) => { - let res = {}; +export const getLimitsInitValues = defaultMemoize( + (limits, tests, environments, exerciseId) => { + let res = {}; + let wallTimeCount = 0; + let cpuTimeCount = 0; + + 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(); + } + + // Prepare time object and aggregate data for heuristics ... + const time = {}; + if (lim && lim['wall-time']) { + time['wall-time'] = String(lim['wall-time']); + ++wallTimeCount; + } + if (lim && lim['cpu-time']) { + time['cpu-time'] = String(lim['cpu-time']); + ++cpuTimeCount; + } + res[testEnc][envId] = { + memory: lim ? String(lim.memory) : '0', + time + }; + }); + }); - 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(); + // Use heuristics to decide, which time will be used, and postprocess the data + const preciseTime = cpuTimeCount >= wallTimeCount; + const primaryTime = preciseTime ? 'cpu-time' : 'wall-time'; + const secondaryTime = preciseTime ? 'wall-time' : 'cpu-time'; + for (const testEnc in res) { + for (const envId in res[testEnc]) { + const time = res[testEnc][envId].time; + res[testEnc][envId].time = + time[primaryTime] !== undefined + ? time[primaryTime] + : time[secondaryTime] !== undefined ? time[secondaryTime] : '0'; } + } - res[testEnc][envId] = { - memory: lim ? String(lim.memory) : '0', - time: lim ? String(lim['wall-time']) : '0' - }; - }); - }); - - return { - limits: res, - preciseTime: true - }; -}; + return { + limits: res, + preciseTime + }; + } +); const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { let res = { @@ -462,23 +482,17 @@ const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { * 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] = transformLimitsObject( - formData.limits[encodeTestId(test.id)][envId] - ); - return acc; - }, {}) - }; - return editEnvironmentSimpleLimits(environment.id, data); - }) - ); +export const transformLimitsValues = (formData, tests, runtimeEnvironments) => + runtimeEnvironments.map(environment => { + const envId = encodeEnvironmentId(environment.id); + const data = { + limits: tests.reduce((acc, test) => { + acc[test.id] = transformLimitsObject( + formData.limits[encodeTestId(test.id)][envId], + formData.preciseTime ? 'cpu-time' : 'wall-time' + ); + return acc; + }, {}) + }; + return { id: environment.id, data }; + }); diff --git a/src/locales/cs.json b/src/locales/cs.json index 395329483..077b5cd92 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -141,6 +141,7 @@ "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.hasThreshold": "Students require cetrain number of points to complete the course", "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:", @@ -375,6 +376,8 @@ "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.preciseTime": "Precise Time Measurement", + "app.editSimpleLimitsForm.preciseTimeTooltip": "If precise time measurement is selected, ReCodEx will measure the consumed CPU time of tested soltions. Otherwise, the wall time will be measured. CPU is better in cases when serial time complexity of the solution is tested and tight time limits are set. Wall time is better in general cases as it better reflects the actual time consumed by the solution (indcluing I/O), but it is more susceptible to errors of measurement.", "app.editSimpleLimitsForm.reset": "Obnovit původní", "app.editSimpleLimitsForm.submit": "Uložit limity", "app.editSimpleLimitsForm.submitting": "Ukládám limity ...", @@ -397,9 +400,12 @@ "app.editTestsTest.remove": "Odebrat", "app.editTestsTest.weight": "Váha testu:", "app.editUser.description": "Upravit nastavení uživatele", + "app.editUser.makeLocal": "Create local account", "app.editUser.title": "Upravit uživatelský profil", "app.editUserProfile.degreesAfterName": "Tituly za jménem:", "app.editUserProfile.degreesBeforeName": "Tituly před jménem:", + "app.editUserProfile.emptyLocalPassword": "Local account does not have a password", + "app.editUserProfile.emptyLocalPasswordExplain": "You may not sign in to ReCodEx using local account until you set the password.", "app.editUserProfile.firstName": "Jméno:", "app.editUserProfile.lastName": "Příjmění:", "app.editUserProfile.passwordInstructions": "Pokud nechcete změnit heslo, nechte tato pole prázná", @@ -627,6 +633,12 @@ "app.failedGroupDetail.msg": "Načtení skupiny se nezdařilo. Prosíme zkuste dotaz opakovat později.", "app.failedSubmissionDetail.description": "Prosíme zkontroluje své připojení k internetu a zkuste dotaz později.", "app.failedSubmissionDetail.title": "Načtení vyhodnocení úlohy se nezdařilo", + "app.failureList.headCreatedAt": "Created at", + "app.failureList.headDescription": "Description", + "app.failureList.headResolutionNote": "Resolution note", + "app.failureList.headResolvedAt": "Resolved at", + "app.failureList.headType": "Type", + "app.failureList.noFailures": "There are no failures in this list.", "app.faq.description": "ReCodEx FAQ", "app.faq.title": "FAQ", "app.feedbackAndBugs.contribution": "If you are interested in frontend web development, feel free to fix the bug itself and send a pull request! Any help will be much appreciated!", @@ -865,6 +877,7 @@ "app.referenceSolutionDetail.exercise": "Exercise", "app.referenceSolutionDetail.refreshEvaluations": "Obnovit", "app.referenceSolutionDetail.resubmit": "Znovu vyhodnotit", + "app.referenceSolutionDetail.resubmitDebug": "Resubmit in Debug Mode", "app.referenceSolutionDetail.title.details": "Detail referenčního řešení", "app.referenceSolutionDetail.uploadedAt": "Nahráno:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", @@ -918,6 +931,7 @@ "app.resultsTable.total": "Celkem", "app.search.query": "Vyhledaný dotaz: ", "app.search.title": "Hledat:", + "app.sidebar.menu.admin.failures": "Submission Failures", "app.sidebar.menu.admin.instances": "Instance", "app.sidebar.menu.admin.users": "Uživatelé", "app.sidebar.menu.createAccount": "Zaregistrovat se", @@ -983,8 +997,24 @@ "app.submissionEvaluation.select": "Select", "app.submissionEvaluation.selected": "Selected", "app.submissionEvaluation.title": "Other submissions of this solution", + "app.submissionFailures.description": "Browse all submission failures", + "app.submissionFailures.failed": "Cannot load the list of failures", + "app.submissionFailures.failedDescription": "We are sorry for the inconvenience, please try again later.", + "app.submissionFailures.listTitle": "Submission Failures", + "app.submissionFailures.loading": "Loading list of failures ...", + "app.submissionFailures.loadingDescription": "Please wait while we are getting the list of failures ready.", + "app.submissionFailures.resolve": "Resolve", + "app.submissionFailures.resolveClose": "Close", + "app.submissionFailures.resolveMaxLengthExceeded": "Maximum length of the note exceeded.", + "app.submissionFailures.resolveNote": "Resolve note:", + "app.submissionFailures.resolveSave": "Save", + "app.submissionFailures.resolveSaving": "Saving ...", + "app.submissionFailures.resolveSuccesss": "Saved", + "app.submissionFailures.resolveTitle": "Resolve Failure", + "app.submissionFailures.title": "Submission Failures", "app.submissionStatus.accepted": "Toto řešení bylo připnuto cvičícím.", "app.submissions.testResultsTable.correctness": "Správnost odevzdaného řešení (rozhodnutí sudího)", + "app.submissions.testResultsTable.cpuTimeExplain": "Procesorový čas (celový čas využití CPU všech spuštěných vláken dohromady)", "app.submissions.testResultsTable.exitCode": "Návratový kód (případně chybová hláška, pokud pro daný kód existuje)", "app.submissions.testResultsTable.memoryExceeded": "Naměřená spotřeba paměti", "app.submissions.testResultsTable.overallTestResult": "Celkové výsledky testu", @@ -992,6 +1022,7 @@ "app.submissions.testResultsTable.statusOK": "OK", "app.submissions.testResultsTable.statusSkipped": "Přeskočeno", "app.submissions.testResultsTable.timeExceeded": "Naměřený čas spuštění", + "app.submissions.testResultsTable.wallTimeExplain": "Reálný čas (tzv. 'wall-time' měřený systémovými hodinami)", "app.submissionsTable.commentsIcon.count": "Počet komentářů: {count}", "app.submissionsTable.commentsIcon.last": "Poslední komentář: {last}", "app.submissionsTable.environment": "Target language", diff --git a/src/locales/en.json b/src/locales/en.json index 876186bdf..8f0d8b39a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -141,6 +141,7 @@ "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.hasThreshold": "Students require cetrain number of points to complete the course", "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:", @@ -375,6 +376,8 @@ "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.preciseTime": "Precise Time Measurement", + "app.editSimpleLimitsForm.preciseTimeTooltip": "If precise time measurement is selected, ReCodEx will measure the consumed CPU time of tested soltions. Otherwise, the wall time will be measured. CPU is better in cases when serial time complexity of the solution is tested and tight time limits are set. Wall time is better in general cases as it better reflects the actual time consumed by the solution (indcluing I/O), but it is more susceptible to errors of measurement.", "app.editSimpleLimitsForm.reset": "Reset", "app.editSimpleLimitsForm.submit": "Save Limits", "app.editSimpleLimitsForm.submitting": "Saving Limits ...", @@ -397,9 +400,12 @@ "app.editTestsTest.remove": "Remove", "app.editTestsTest.weight": "Test weight:", "app.editUser.description": "Edit user's profile", + "app.editUser.makeLocal": "Create local account", "app.editUser.title": "Edit user's profile", "app.editUserProfile.degreesAfterName": "Degrees after name:", "app.editUserProfile.degreesBeforeName": "Degrees before name:", + "app.editUserProfile.emptyLocalPassword": "Local account does not have a password", + "app.editUserProfile.emptyLocalPasswordExplain": "You may not sign in to ReCodEx using local account until you set the password.", "app.editUserProfile.firstName": "First name:", "app.editUserProfile.lastName": "Last name:", "app.editUserProfile.passwordInstructions": "If you don't want to change your password leave these inputs blank", @@ -627,6 +633,12 @@ "app.failedGroupDetail.msg": "Cannot load group detail. Please try again later.", "app.failedSubmissionDetail.description": "Make sure you are connected to the Internet and repeat the action after a while.", "app.failedSubmissionDetail.title": "Cannot load evaluation of the solution", + "app.failureList.headCreatedAt": "Created at", + "app.failureList.headDescription": "Description", + "app.failureList.headResolutionNote": "Resolution note", + "app.failureList.headResolvedAt": "Resolved at", + "app.failureList.headType": "Type", + "app.failureList.noFailures": "There are no failures in this list.", "app.faq.description": "ReCodEx FAQ", "app.faq.title": "FAQ", "app.feedbackAndBugs.contribution": "If you are interested in frontend web development, feel free to fix the bug itself and send a pull request! Any help will be much appreciated!", @@ -865,6 +877,7 @@ "app.referenceSolutionDetail.exercise": "Exercise", "app.referenceSolutionDetail.refreshEvaluations": "Refresh", "app.referenceSolutionDetail.resubmit": "Resubmit", + "app.referenceSolutionDetail.resubmitDebug": "Resubmit in Debug Mode", "app.referenceSolutionDetail.title.details": "Reference solution detail", "app.referenceSolutionDetail.uploadedAt": "Uploaded at:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", @@ -918,6 +931,7 @@ "app.resultsTable.total": "Total", "app.search.query": "Searched query: ", "app.search.title": "Search:", + "app.sidebar.menu.admin.failures": "Submission Failures", "app.sidebar.menu.admin.instances": "Instances", "app.sidebar.menu.admin.users": "Users", "app.sidebar.menu.createAccount": "Create account", @@ -983,8 +997,24 @@ "app.submissionEvaluation.select": "Select", "app.submissionEvaluation.selected": "Selected", "app.submissionEvaluation.title": "Other submissions of this solution", + "app.submissionFailures.description": "Browse all submission failures", + "app.submissionFailures.failed": "Cannot load the list of failures", + "app.submissionFailures.failedDescription": "We are sorry for the inconvenience, please try again later.", + "app.submissionFailures.listTitle": "Submission Failures", + "app.submissionFailures.loading": "Loading list of failures ...", + "app.submissionFailures.loadingDescription": "Please wait while we are getting the list of failures ready.", + "app.submissionFailures.resolve": "Resolve", + "app.submissionFailures.resolveClose": "Close", + "app.submissionFailures.resolveMaxLengthExceeded": "Maximum length of the note exceeded.", + "app.submissionFailures.resolveNote": "Resolve note:", + "app.submissionFailures.resolveSave": "Save", + "app.submissionFailures.resolveSaving": "Saving ...", + "app.submissionFailures.resolveSuccesss": "Saved", + "app.submissionFailures.resolveTitle": "Resolve Failure", + "app.submissionFailures.title": "Submission Failures", "app.submissionStatus.accepted": "This solution was marked by one of the supervisors as accepted.", "app.submissions.testResultsTable.correctness": "Correctness of the result (verdict of the judge)", + "app.submissions.testResultsTable.cpuTimeExplain": "CPU time (total time the CPU was used by all threads)", "app.submissions.testResultsTable.exitCode": "Exit code (possibly translated into error message if translation is available)", "app.submissions.testResultsTable.memoryExceeded": "Measured memory utilization", "app.submissions.testResultsTable.overallTestResult": "Overall test result", @@ -992,6 +1022,7 @@ "app.submissions.testResultsTable.statusOK": "OK", "app.submissions.testResultsTable.statusSkipped": "SKIPPED", "app.submissions.testResultsTable.timeExceeded": "Measured execution time", + "app.submissions.testResultsTable.wallTimeExplain": "Wall time (real-time measured by external clock)", "app.submissionsTable.commentsIcon.count": "Total Comments: {count}", "app.submissionsTable.commentsIcon.last": "Last Comment: {last}", "app.submissionsTable.environment": "Target language", diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index b92da1500..98569bca4 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage, injectIntl } from 'react-intl'; import { Row, Col } from 'react-bootstrap'; import { connect } from 'react-redux'; +import { defaultMemoize } from 'reselect'; import Page from '../../components/layout/Page'; import Box from '../../components/widgets/Box'; @@ -64,18 +65,19 @@ import { getEnvInitValues, transformEnvValues, getTestsInitValues, - transformAndSendTestsValues, + transformTestsValues, getSimpleConfigInitValues, - transformAndSendConfigValues, + transformConfigValues, getLimitsInitValues, - transformAndSendLimitsValues + transformLimitsValues } from '../../helpers/exerciseSimpleForm'; class EditExerciseSimpleConfig extends Component { componentWillMount = () => this.props.loadAsync(); - componentWillReceiveProps = props => { - if (this.props.params.exerciseId !== props.params.exerciseId) { - props.loadAsync(); + + componentWillReceiveProps = nextProps => { + if (this.props.params.exerciseId !== nextProps.params.exerciseId) { + nextProps.loadAsync(); } }; @@ -101,6 +103,73 @@ class EditExerciseSimpleConfig extends Component { dispatch(fetchPipelines()) ]); + transformAndSendTestsValues = data => { + const { + editTests, + editScoreConfig, + fetchConfig, + fetchEnvironmentSimpleLimits + } = this.props; + + const { tests, scoreConfig } = transformTestsValues(data); + + return Promise.all([ + editTests({ tests }), + editScoreConfig({ scoreConfig }) + ]).then(() => Promise.all([fetchConfig(), fetchEnvironmentSimpleLimits()])); + }; + + transformAndSendConfigValuesCreator = defaultMemoize( + (pipelines, environments, tests, config) => { + const { setConfig } = this.props; + return data => + setConfig( + transformConfigValues(data, pipelines, environments, tests, config) + ); + } + ); + + transformAndSendEnvValues = defaultMemoize( + (exerciseId, pipelines, environments, tests, config) => { + const { + editEnvironmentConfigs, + reloadConfigAndLimits, + setConfig + } = this.props; + + return data => { + const newEnvironments = transformEnvValues(data, environments); + const configData = transformConfigValues( + getSimpleConfigInitValues(config, tests), + pipelines, + newEnvironments, + tests, + config + ); + return editEnvironmentConfigs({ + environmentConfigs: newEnvironments + }) + .then(() => setConfig(configData)) + .then(reloadConfigAndLimits(exerciseId)); + }; + } + ); + + transformAndSendLimitsValues = defaultMemoize( + (tests, exerciseRuntimeEnvironments) => { + const { editEnvironmentSimpleLimits } = this.props; + return formData => + Promise.all( + transformLimitsValues( + formData, + tests, + exerciseRuntimeEnvironments, + editEnvironmentSimpleLimits + ).map(({ id, data }) => editEnvironmentSimpleLimits(id, data)) + ); + } + ); + render() { const { links: { EXERCISE_URI_FACTORY }, @@ -108,28 +177,20 @@ class EditExerciseSimpleConfig extends Component { 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={ - {exercise => + {(exercise, tests) =>
- -
- - } - unlimitedHeight - > - - {(scoreConfig, tests) => - - transformAndSendTestsValues( - data, - editTests, - editScoreConfig - ).then(() => - Promise.all([ - fetchConfig(), - fetchEnvironmentSimpleLimits() - ]) - )} - />} - - - - } - unlimitedHeight - > - - {(config, tests, environmentConfigs, ...pipelines) => - + {( + pipelines // pipelines are returned as a whole array (so they can be cached properly) + ) => +
+ +
+ + } + unlimitedHeight + > + + {scoreConfig => + } + + + + } + unlimitedHeight > - {(...environments) => - { - const newEnvironments = transformEnvValues( - data, - environments - ); - return editEnvironmentConfigs({ - environmentConfigs: newEnvironments - }) - .then(() => - transformAndSendConfigValues( - getSimpleConfigInitValues(config, tests), + + {(config, environmentConfigs) => + + {environments => + } - } - - - - - - - -
+ config + )} + />} + } + + + +
+ + + +
- -
- - {(config, tests, environments, ...pipelines) => - tests.length > 0 - ? - transformAndSendConfigValues( - data, - pipelines, - environments, - tests, - config, - setConfig - )} - /> - :
-

- {' '} - -

- -
} -
- - -
+ +
+ + {(config, environments) => + tests.length > 0 + ? + :
+

+ {' '} + +

+ +
} +
+ + +
+ } +
- {(tests, envConfig) => + {envConfig => tests.length > 0 && exercise.runtimeEnvironments.length > 0 ? - transformAndSendLimitsValues( - data, - tests, - exercise.runtimeEnvironments, - editEnvironmentSimpleLimits - )} + onSubmit={this.transformAndSendLimitsValues( + tests, + exercise.runtimeEnvironments + )} environments={exercise.runtimeEnvironments} tests={tests} initialValues={getLimitsInitValues( @@ -386,6 +416,21 @@ EditExerciseSimpleConfig.propTypes = { intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; +const cloneVerticallyWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneVertically(formName, testName, runtimeEnvironmentId, field)) +); + +const cloneHorizontallyWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneHorizontally(formName, testName, runtimeEnvironmentId, field)) +); + +const cloneAllWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)) +); + export default injectIntl( withLinks( connect( @@ -432,25 +477,9 @@ export default injectIntl( 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)), - + cloneVertically: cloneVerticallyWrapper(dispatch), + cloneHorizontally: cloneHorizontallyWrapper(dispatch), + cloneAll: cloneAllWrapper(dispatch), reloadConfigAndLimits: exerciseId => () => dispatch(fetchExercise(exerciseId)).then(({ value: exercise }) => Promise.all([ diff --git a/src/redux/modules/simpleLimits.js b/src/redux/modules/simpleLimits.js index 9e9377408..a485c63fa 100644 --- a/src/redux/modules/simpleLimits.js +++ b/src/redux/modules/simpleLimits.js @@ -80,7 +80,7 @@ const getTargetFormKeys = ( formName, // form identifier testId, // test id or null (if all test should be targetted) environmentId, // environment ID or null (if all environments should be targetted) - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => { const testEnc = testId ? encodeTestId(testId) : null; const envEnc = environmentId ? encodeEnvironmentId(environmentId) : null; @@ -101,7 +101,7 @@ export const cloneVertically = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( @@ -127,7 +127,7 @@ export const cloneHorizontally = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( @@ -153,7 +153,7 @@ export const cloneAll = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js index f3f35a964..b2aca995c 100644 --- a/src/redux/selectors/simpleLimits.js +++ b/src/redux/selectors/simpleLimits.js @@ -1,37 +1,7 @@ import { createSelector } from 'reselect'; -import { getExerciseRuntimeEnvironments } from './exercises'; -import { endpointDisguisedAsIdFactory } from '../modules/simpleLimits'; const getLimits = state => state.simpleLimits; -const EMPTY_OBJ = {}; export const simpleLimitsSelector = createSelector(getLimits, limits => limits.get('resources') ); - -export const simpleLimitsAllSelector = exerciseId => - createSelector( - [getLimits, getExerciseRuntimeEnvironments(exerciseId)], - (limits, runtimeEnvironments) => { - if (!limits || !runtimeEnvironments) { - return EMPTY_OBJ; - } - - return runtimeEnvironments.reduce((acc, runtimeEnvironment) => { - const runtimeEnvironmentId = runtimeEnvironment.get('id'); - let testLimits = limits.getIn([ - 'resources', - endpointDisguisedAsIdFactory({ - exerciseId, - runtimeEnvironmentId - }), - 'data' - ]); - if (testLimits) { - testLimits = testLimits.toJS(); - } - acc[runtimeEnvironmentId] = testLimits; - return acc; - }, {}); - } - ); diff --git a/src/redux/selectors/supplementaryFiles.js b/src/redux/selectors/supplementaryFiles.js index ea6d6c260..7d84b8ba1 100644 --- a/src/redux/selectors/supplementaryFiles.js +++ b/src/redux/selectors/supplementaryFiles.js @@ -1,5 +1,6 @@ -import { createSelector } from 'reselect'; +import { createSelector, defaultMemoize } from 'reselect'; import { isReady } from '../helpers/resourceManager'; +import { getExercise } from './exercises'; const getSupplementaryFiles = state => state.supplementaryFiles; export const getSupplementaryFile = id => @@ -16,3 +17,19 @@ export const createGetSupplementaryFiles = ids => .filter(isReady) .filter(file => ids.indexOf(file.getIn(['data', 'id'])) >= 0) ); + +export const getSupplementaryFilesForExercise = defaultMemoize(exerciseId => + createSelector( + [getExercise(exerciseId), supplementaryFilesSelector], + (exercise, supplementaryFiles) => { + const ids = exercise && exercise.getIn(['data', 'supplementaryFilesIds']); + return ( + ids && + supplementaryFiles && + supplementaryFiles + .filter(isReady) + .filter(file => ids.indexOf(file.getIn(['data', 'id'])) >= 0) + ); + } + ) +); diff --git a/views/index.ejs b/views/index.ejs index e8ec9523a..c1e430a85 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -9,15 +9,9 @@