From 41f4a6fc8c19b23844fd8194c636f9014b8cfb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Sat, 24 Feb 2018 23:17:41 +0100 Subject: [PATCH] Hwgroup selection on the edit limits page (#184) * Test limits were moved to a separate page. * Fixing links and button captions. * Massive reorg. of low level technicalities and helpers. * Limit form (and related stuff) modified to use hwGroup limits endpoint instead of simpleLimits endpoint. Simple limits are no more. * Cleanup - removing unused old components. * Remaining SimpleLimits components renamed to Limits only. * Fixing state-detection bug in multiple forms (spread by copy-and-paste). * Edit hardware group form added to Edit Limits page. * Adding metadata panel on Edit Limits page, so the limits constraints for selected hardware group are visible. * Final polishing and minor tune-ups. --- .../AdminAssignmentsTableRow.js | 2 +- .../AssignmentTableRow/AssignmentTableRow.js | 2 +- .../SubmissionsTable/SubmissionsTable.js | 2 +- .../Exercises/EditLimitsBox/EditLimitsBox.js | 38 -- .../Exercises/EditLimitsBox/index.js | 1 - .../ExerciseButtons/ExerciseButtons.js | 69 +++ .../Exercises/ExerciseButtons/index.js | 1 + .../ExerciseDetail/ExerciseDetail.js | 2 +- .../ExercisesListItem/ExercisesListItem.js | 8 +- .../Exercises/ExercisesName/ExercisesName.js | 2 +- .../ExercisesSimpleListItem.js | 2 +- .../FilesTable/AttachmentFilesTableRow.js | 7 +- .../FilesTable/SupplementaryFilesTableRow.js | 5 +- .../HardwareGroupMetadata.js | 228 +++++++++ .../Exercises/HardwareGroupMetadata/index.js | 1 + .../Groups/AdminsView/AdminsView.js | 5 +- src/components/Groups/GroupTree/GroupTree.js | 2 +- .../Groups/GroupsList/GroupsList.js | 2 +- .../Groups/GroupsName/GroupsName.js | 2 +- .../Groups/ResultsTable/ResultsTable.js | 2 +- .../Groups/SupervisorsView/SupervisorsView.js | 70 ++- .../InstancesTable/InstancesTable.js | 2 +- .../PipelinesListItem/PipelinesListItem.js | 2 +- .../PipelinesSimpleListItem.js | 2 +- .../ReferenceSolutionEvaluations.js | 2 +- .../FailuresListItem/FailuresListItem.js | 2 +- .../SubmissionEvaluations.js | 2 +- .../SubmissionStatus/SubmissionStatus.js | 2 +- src/components/Users/UsersName/UsersName.js | 2 +- src/components/Users/UsersName/usersName.less | 1 + .../DeleteButton/ConfirmDeleteButton.js | 2 +- .../buttons/DeleteButton/DeletedButton.js | 10 +- .../buttons/DeleteButton/DeletingButton.js | 13 +- .../DeleteButton/DeletingFailedButton.js | 4 +- .../EditAssignmentForm/EditAssignmentForm.js | 12 +- .../EditEnvironmentConfigForm.js | 14 +- .../EditEnvironmentSimpleForm.js | 14 +- .../EditExerciseConfigForm.js | 14 +- .../EditExerciseForm/EditExerciseForm.js | 28 +- .../EditExerciseSimpleConfigForm.css | 1 + .../EditExerciseSimpleConfigForm.js | 8 +- .../EditHardwareGroupForm.js | 132 ++++++ .../forms/EditHardwareGroupForm/index.js | 1 + .../EditHardwareGroupLimits.js | 29 -- .../HardwareGroupFields.js | 88 ---- .../forms/EditHardwareGroupLimits/index.js | 1 - .../EditLimits/EditEnvironmentLimitsFields.js | 70 --- .../EditLimits/EditEnvironmentLimitsForm.js | 190 -------- src/components/forms/EditLimits/EditLimits.js | 52 -- src/components/forms/EditLimits/index.js | 1 - src/components/forms/EditLimits/styles.less | 10 - .../EditLimitsForm.js} | 98 ++-- src/components/forms/EditLimitsForm/index.js | 1 + .../styles.less | 0 .../EditPipelineForm/EditPipelineForm.js | 12 +- .../EditScoreConfigForm.js | 14 +- .../forms/EditSimpleLimitsForm/index.js | 1 - .../forms/EditTestsForm/EditTestsForm.js | 16 +- src/components/forms/Fields/DatetimeField.js | 2 +- .../forms/Fields/EditLimitsField.js | 269 +++++++++++ ...eLimitsField.less => EditLimitsField.less} | 0 .../forms/Fields/EditSimpleLimitsField.js | 287 ----------- .../forms/Fields/KiloBytesTextField.js | 28 -- src/components/forms/Fields/LimitsField.js | 83 ---- .../forms/Fields/LimitsValueField.js | 2 +- .../forms/Fields/SecondsTextField.js | 33 -- src/components/forms/Fields/index.js | 5 +- .../ForkExerciseForm/ForkExerciseForm.js | 16 +- .../ForkPipelineForm/ForkPipelineForm.js | 16 +- .../SisBindGroupForm/SisBindGroupForm.js | 14 +- .../SisCreateGroupForm/SisCreateGroupForm.js | 14 +- src/components/layout/Sidebar/Admin.js | 2 +- src/components/layout/Sidebar/LoggedIn.js | 2 +- src/components/layout/Sidebar/Public.js | 2 +- src/components/layout/Sidebar/Student.js | 2 +- src/components/layout/Sidebar/Supervisor.js | 2 +- src/components/widgets/Badge/Badge.js | 2 +- .../widgets/Breadcrumbs/BreadcrumbItem.js | 2 +- .../widgets/Comments/Comment/Comment.js | 2 +- src/components/widgets/Header/Header.js | 2 +- .../BadgeContainer/BadgeContainer.js | 2 +- .../CAS/AuthenticationButtonContainer.js | 2 +- .../ResubmitSolutionContainer.js | 2 +- .../SisIntegrationContainer.js | 2 +- .../SisSupervisorGroupsContainer.js | 2 +- .../SubmitSolutionContainer.js | 2 +- .../UsersNameContainer/UsersNameContainer.js | 2 +- src/helpers/common.js | 28 ++ src/helpers/exerciseLimits.js | 236 +++++++++ src/helpers/exerciseSimpleForm.js | 120 +---- src/{hoc => helpers}/withLinks.js | 0 src/links/index.js | 3 + src/locales/cs.json | 95 ++-- src/locales/en.json | 71 ++- src/pages/Assignment/Assignment.js | 2 +- src/pages/ChangePassword/ChangePassword.js | 2 +- src/pages/Dashboard/Dashboard.js | 5 +- src/pages/EditAssignment/EditAssignment.js | 2 +- src/pages/EditExercise/EditExercise.js | 8 +- .../EditExerciseConfig/EditExerciseConfig.js | 40 +- .../EditExerciseLimits/EditExerciseLimits.js | 446 ++++++++++++++++++ src/pages/EditExerciseLimits/index.js | 1 + .../EditExerciseSimpleConfig.js | 236 ++------- src/pages/EditGroup/EditGroup.js | 2 +- src/pages/EditInstance/EditInstance.js | 2 +- src/pages/EditPipeline/EditPipeline.js | 2 +- .../EmailVerification/EmailVerification.js | 2 +- src/pages/Exercise/Exercise.js | 118 ++--- src/pages/Exercises/Exercises.js | 32 +- src/pages/FAQ/FAQ.js | 2 +- src/pages/FeedbackAndBugs/FeedbackAndBugs.js | 2 +- src/pages/Group/Group.js | 2 +- src/pages/Instance/Instance.js | 8 +- src/pages/Login/Login.js | 2 +- src/pages/Pipeline/Pipeline.js | 2 +- src/pages/Pipelines/Pipelines.js | 2 +- .../ReferenceSolution/ReferenceSolution.js | 2 +- .../ReferenceSolutionEvaluation.js | 2 +- src/pages/Registration/Registration.js | 2 +- src/pages/ResetPassword/ResetPassword.js | 2 +- src/pages/SisIntegration/SisIntegration.js | 2 +- src/pages/User/User.js | 2 +- src/pages/Users/Users.js | 2 +- src/pages/routes.js | 2 + src/redux/modules/exerciseConfigs.js | 8 +- src/redux/modules/exercises.js | 133 +++--- src/redux/modules/limits.js | 130 +++++ src/redux/modules/simpleLimits.js | 178 ------- src/redux/reducer.js | 2 - src/redux/selectors/assignments.js | 6 +- src/redux/selectors/attachmentFiles.js | 4 +- src/redux/selectors/exercises.js | 4 +- src/redux/selectors/groups.js | 12 +- src/redux/selectors/instances.js | 3 +- src/redux/selectors/limits.js | 16 +- src/redux/selectors/simpleLimits.js | 7 - src/redux/selectors/sisSupervisedCourses.js | 3 +- src/redux/selectors/stats.js | 6 +- src/redux/selectors/supplementaryFiles.js | 5 +- src/redux/selectors/users.js | 40 +- src/redux/selectors/usersGroups.js | 10 +- views/index.ejs | 18 +- 142 files changed, 2155 insertions(+), 2026 deletions(-) delete mode 100644 src/components/Exercises/EditLimitsBox/EditLimitsBox.js delete mode 100644 src/components/Exercises/EditLimitsBox/index.js create mode 100644 src/components/Exercises/ExerciseButtons/ExerciseButtons.js create mode 100644 src/components/Exercises/ExerciseButtons/index.js create mode 100644 src/components/Exercises/HardwareGroupMetadata/HardwareGroupMetadata.js create mode 100644 src/components/Exercises/HardwareGroupMetadata/index.js create mode 100644 src/components/forms/EditHardwareGroupForm/EditHardwareGroupForm.js create mode 100644 src/components/forms/EditHardwareGroupForm/index.js delete mode 100644 src/components/forms/EditHardwareGroupLimits/EditHardwareGroupLimits.js delete mode 100644 src/components/forms/EditHardwareGroupLimits/HardwareGroupFields.js delete mode 100644 src/components/forms/EditHardwareGroupLimits/index.js delete mode 100644 src/components/forms/EditLimits/EditEnvironmentLimitsFields.js delete mode 100644 src/components/forms/EditLimits/EditEnvironmentLimitsForm.js delete mode 100644 src/components/forms/EditLimits/EditLimits.js delete mode 100644 src/components/forms/EditLimits/index.js delete mode 100644 src/components/forms/EditLimits/styles.less rename src/components/forms/{EditSimpleLimitsForm/EditSimpleLimitsForm.js => EditLimitsForm/EditLimitsForm.js} (73%) create mode 100644 src/components/forms/EditLimitsForm/index.js rename src/components/forms/{EditSimpleLimitsForm => EditLimitsForm}/styles.less (100%) delete mode 100644 src/components/forms/EditSimpleLimitsForm/index.js create mode 100644 src/components/forms/Fields/EditLimitsField.js rename src/components/forms/Fields/{EditSimpleLimitsField.less => EditLimitsField.less} (100%) delete mode 100644 src/components/forms/Fields/EditSimpleLimitsField.js delete mode 100644 src/components/forms/Fields/KiloBytesTextField.js delete mode 100644 src/components/forms/Fields/LimitsField.js delete mode 100644 src/components/forms/Fields/SecondsTextField.js create mode 100644 src/helpers/common.js create mode 100644 src/helpers/exerciseLimits.js rename src/{hoc => helpers}/withLinks.js (100%) create mode 100644 src/pages/EditExerciseLimits/EditExerciseLimits.js create mode 100644 src/pages/EditExerciseLimits/index.js delete mode 100644 src/redux/modules/simpleLimits.js delete mode 100644 src/redux/selectors/simpleLimits.js diff --git a/src/components/Assignments/AdminAssignmentsTable/AdminAssignmentsTableRow.js b/src/components/Assignments/AdminAssignmentsTable/AdminAssignmentsTableRow.js index 357d3e928..51c752b26 100644 --- a/src/components/Assignments/AdminAssignmentsTable/AdminAssignmentsTableRow.js +++ b/src/components/Assignments/AdminAssignmentsTable/AdminAssignmentsTableRow.js @@ -7,7 +7,7 @@ import Button from '../../widgets/FlatButton'; import { LinkContainer } from 'react-router-bootstrap'; import DeleteAssignmentButtonContainer from '../../../containers/DeleteAssignmentButtonContainer'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import { EditIcon, diff --git a/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js b/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js index dc08ce878..13d6ea014 100644 --- a/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js +++ b/src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js @@ -5,7 +5,7 @@ import AssignmentStatusIcon from '../AssignmentStatusIcon/AssignmentStatusIcon'; import { FormattedDate, FormattedTime } from 'react-intl'; import ResourceRenderer from '../../../helpers/ResourceRenderer'; -import withLinks from '../../../../hoc/withLinks'; +import withLinks from '../../../../helpers/withLinks'; import { LocalizedExerciseName } from '../../../helpers/LocalizedNames'; import { MaybeBonusAssignmentIcon } from '../../../icons'; diff --git a/src/components/Assignments/SubmissionsTable/SubmissionsTable.js b/src/components/Assignments/SubmissionsTable/SubmissionsTable.js index 3be897e6f..513c4a504 100644 --- a/src/components/Assignments/SubmissionsTable/SubmissionsTable.js +++ b/src/components/Assignments/SubmissionsTable/SubmissionsTable.js @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import { OverlayTrigger, Tooltip, Table } from 'react-bootstrap'; import Box from '../../widgets/Box'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import LoadingSubmissionTableRow from './LoadingSubmissionTableRow'; diff --git a/src/components/Exercises/EditLimitsBox/EditLimitsBox.js b/src/components/Exercises/EditLimitsBox/EditLimitsBox.js deleted file mode 100644 index 00ee4abae..000000000 --- a/src/components/Exercises/EditLimitsBox/EditLimitsBox.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { Tabs, Tab } from 'react-bootstrap'; - -import Box from '../../widgets/Box'; -import EditLimits from '../../forms/EditLimits'; - -const EditLimitsBox = ({ hardwareGroups, editLimits, limits, ...props }) => - - } - unlimitedHeight - > - - {hardwareGroups.filter(({ isAvailable }) => isAvailable).map(({ id }) => - - - - )} - - ; - -EditLimitsBox.propTypes = { - hardwareGroups: PropTypes.array.isRequired, - editLimits: PropTypes.func.isRequired, - limits: PropTypes.func.isRequired -}; - -export default EditLimitsBox; diff --git a/src/components/Exercises/EditLimitsBox/index.js b/src/components/Exercises/EditLimitsBox/index.js deleted file mode 100644 index 42bfef20c..000000000 --- a/src/components/Exercises/EditLimitsBox/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './EditLimitsBox'; diff --git a/src/components/Exercises/ExerciseButtons/ExerciseButtons.js b/src/components/Exercises/ExerciseButtons/ExerciseButtons.js new file mode 100644 index 000000000..d031893bd --- /dev/null +++ b/src/components/Exercises/ExerciseButtons/ExerciseButtons.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { ButtonGroup } from 'react-bootstrap'; +import { LinkContainer } from 'react-router-bootstrap'; + +import Button from '../../widgets/FlatButton'; +import { EditIcon } from '../../icons'; +// import Confirm from '../../components/forms/Confirm'; +// import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; + +import withLinks from '../../../helpers/withLinks'; + +const ExerciseButtons = ({ + exerciseId, + links: { + EXERCISE_EDIT_URI_FACTORY, + EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, + EXERCISE_EDIT_LIMITS_URI_FACTORY + } +}) => +
+ + + + + + + + + + + {/* forkExercise(forkId, formData)} +/> */} + +

+

; + +ExerciseButtons.propTypes = { + exerciseId: PropTypes.string.isRequired, + links: PropTypes.object +}; + +export default withLinks(ExerciseButtons); diff --git a/src/components/Exercises/ExerciseButtons/index.js b/src/components/Exercises/ExerciseButtons/index.js new file mode 100644 index 000000000..f61585911 --- /dev/null +++ b/src/components/Exercises/ExerciseButtons/index.js @@ -0,0 +1 @@ +export default from './ExerciseButtons'; diff --git a/src/components/Exercises/ExerciseDetail/ExerciseDetail.js b/src/components/Exercises/ExerciseDetail/ExerciseDetail.js index cc62254e4..df3d1384e 100644 --- a/src/components/Exercises/ExerciseDetail/ExerciseDetail.js +++ b/src/components/Exercises/ExerciseDetail/ExerciseDetail.js @@ -12,7 +12,7 @@ import { Link } from 'react-router'; import Box from '../../widgets/Box'; import DifficultyIcon from '../DifficultyIcon'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import UsersNameContainer from '../../../containers/UsersNameContainer'; import GroupsNameContainer from '../../../containers/GroupsNameContainer'; import styles from './ExerciseDetail.less'; diff --git a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js index dfccb8231..801d334a4 100644 --- a/src/components/Exercises/ExercisesListItem/ExercisesListItem.js +++ b/src/components/Exercises/ExercisesListItem/ExercisesListItem.js @@ -7,7 +7,7 @@ import UsersNameContainer from '../../../containers/UsersNameContainer'; import GroupsNameContainer from '../../../containers/GroupsNameContainer'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import { ExercisePrefixIcons } from '../../icons'; @@ -47,10 +47,10 @@ const ExercisesListItem = ({ ) - : + : } diff --git a/src/components/Exercises/ExercisesName/ExercisesName.js b/src/components/Exercises/ExercisesName/ExercisesName.js index 236afe9a0..fb6b917e9 100644 --- a/src/components/Exercises/ExercisesName/ExercisesName.js +++ b/src/components/Exercises/ExercisesName/ExercisesName.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import { ExercisePrefixIcons } from '../../icons'; diff --git a/src/components/Exercises/ExercisesSimpleListItem/ExercisesSimpleListItem.js b/src/components/Exercises/ExercisesSimpleListItem/ExercisesSimpleListItem.js index 832e162b8..43bf395c8 100644 --- a/src/components/Exercises/ExercisesSimpleListItem/ExercisesSimpleListItem.js +++ b/src/components/Exercises/ExercisesSimpleListItem/ExercisesSimpleListItem.js @@ -4,7 +4,7 @@ import DifficultyIcon from '../DifficultyIcon'; import UsersNameContainer from '../../../containers/UsersNameContainer'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import { ExercisePrefixIcons } from '../../icons'; diff --git a/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js b/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js index 6583449b2..4351b6fda 100644 --- a/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js +++ b/src/components/Exercises/FilesTable/AttachmentFilesTableRow.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { prettyPrintBytes } from '../../helpers/stringFormatters'; import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { Button } from 'react-bootstrap'; import Confirm from '../../../components/forms/Confirm'; import { DeleteIcon } from '../../../components/icons'; @@ -47,10 +47,7 @@ const AttachmentFilesTableRow = ({ > } diff --git a/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js b/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js index 9473633a0..99afc5b77 100644 --- a/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js +++ b/src/components/Exercises/FilesTable/SupplementaryFilesTableRow.js @@ -50,10 +50,7 @@ const SupplementaryFilesTableRow = ({ > } } diff --git a/src/components/Exercises/HardwareGroupMetadata/HardwareGroupMetadata.js b/src/components/Exercises/HardwareGroupMetadata/HardwareGroupMetadata.js new file mode 100644 index 000000000..ba978fa47 --- /dev/null +++ b/src/components/Exercises/HardwareGroupMetadata/HardwareGroupMetadata.js @@ -0,0 +1,228 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Table, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import Icon from 'react-fontawesome'; + +import prettyMs from 'pretty-ms'; +import { prettyPrintBytes } from '../../helpers/stringFormatters'; +import Box from '../../widgets/Box'; +import { getLimitsConstraintsOfSingleGroup } from '../../../helpers/exerciseLimits'; + +const HardwareGroupMetadata = ({ hardwareGroup, isSuperAdmin = false }) => { + const constraints = getLimitsConstraintsOfSingleGroup(hardwareGroup); + return ( + + } + description={ +
+ {hardwareGroup.name} +
+ } + type="primary" + unlimitedHeight + > + + + {Boolean(isSuperAdmin) && + + + + } + {Boolean(isSuperAdmin) && + + + + } + + + + + + + + + + + + + +
+ + + + {hardwareGroup.id} + +
+ + + {hardwareGroup.description} +
+ + + + + + + } + > + +    + {prettyPrintBytes(constraints.memory.min * 1024)} ...{' '} + {prettyPrintBytes(constraints.memory.max * 1024)} + +
+ + + {constraints.cpuTimePerTest.min === + constraints.wallTimePerTest.min && + constraints.cpuTimePerTest.max === constraints.wallTimePerTest.max + ? + + + + } + > + +    + {prettyMs(constraints.cpuTimePerTest.min * 1000)} ...{' '} + {prettyMs(constraints.cpuTimePerTest.max * 1000)} + + : + + + + } + > + +    + {prettyMs(constraints.cpuTimePerTest.min * 1000)} ...{' '} + {prettyMs(constraints.cpuTimePerTest.max * 1000)} +      + + + + } + > + +    + {prettyMs(constraints.wallTimePerTest.min * 1000)} ...{' '} + {prettyMs(constraints.wallTimePerTest.max * 1000)} + } +
+ + + {constraints.cpuTimePerExercise.min === + constraints.wallTimePerExercise.min && + constraints.cpuTimePerExercise.max !== + constraints.wallTimePerExercise.max + ? + + + + } + > + +    + {prettyMs( + constraints.cpuTimePerExercise.min * 1000 + )} ... {prettyMs(constraints.cpuTimePerExercise.max * 1000)} + + : + + + + } + > + +    + {prettyMs( + constraints.cpuTimePerExercise.min * 1000 + )} ... {prettyMs(constraints.cpuTimePerExercise.max * 1000)} +      + + + + } + > + +    + {prettyMs( + constraints.wallTimePerExercise.min * 1000 + )} ...{' '} + {prettyMs(constraints.wallTimePerExercise.max * 1000)} + } +
+
+ ); +}; + +HardwareGroupMetadata.propTypes = { + hardwareGroup: PropTypes.object.isRequired, + isSuperAdmin: PropTypes.bool, + links: PropTypes.object +}; + +export default HardwareGroupMetadata; diff --git a/src/components/Exercises/HardwareGroupMetadata/index.js b/src/components/Exercises/HardwareGroupMetadata/index.js new file mode 100644 index 000000000..acd750e6b --- /dev/null +++ b/src/components/Exercises/HardwareGroupMetadata/index.js @@ -0,0 +1 @@ +export default from './HardwareGroupMetadata'; diff --git a/src/components/Groups/AdminsView/AdminsView.js b/src/components/Groups/AdminsView/AdminsView.js index e7bd76d7e..90a0280e5 100644 --- a/src/components/Groups/AdminsView/AdminsView.js +++ b/src/components/Groups/AdminsView/AdminsView.js @@ -5,13 +5,12 @@ import { Row, Col } from 'react-bootstrap'; import { getFormValues } from 'redux-form'; import { connect } from 'react-redux'; +import { EMPTY_OBJ } from '../../../helpers/common'; import Box from '../../widgets/Box'; import AddSupervisor from '../AddSupervisor'; import EditGroupForm from '../../forms/EditGroupForm'; import { getLocalizedName } from '../../../helpers/getLocalizedData'; -const EMPTY_OBJECT = {}; - const AdminsView = ({ group, addSubgroup, formValues, intl: { locale } }) =>
@@ -44,7 +43,7 @@ const AdminsView = ({ group, addSubgroup, formValues, intl: { locale } }) => diff --git a/src/components/Groups/GroupsList/GroupsList.js b/src/components/Groups/GroupsList/GroupsList.js index 06ffe34c6..a5f545fb5 100644 --- a/src/components/Groups/GroupsList/GroupsList.js +++ b/src/components/Groups/GroupsList/GroupsList.js @@ -6,7 +6,7 @@ import ResourceRenderer from '../../helpers/ResourceRenderer'; import Icon from 'react-fontawesome'; import GroupsName from '../GroupsName'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const GroupsList = ({ groups = [], diff --git a/src/components/Groups/GroupsName/GroupsName.js b/src/components/Groups/GroupsName/GroupsName.js index dbc059c62..f3080eeae 100644 --- a/src/components/Groups/GroupsName/GroupsName.js +++ b/src/components/Groups/GroupsName/GroupsName.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { LocalizedGroupName } from '../../helpers/LocalizedNames'; const GroupsName = ({ diff --git a/src/components/Groups/ResultsTable/ResultsTable.js b/src/components/Groups/ResultsTable/ResultsTable.js index f078c3e5b..ed454ab48 100644 --- a/src/components/Groups/ResultsTable/ResultsTable.js +++ b/src/components/Groups/ResultsTable/ResultsTable.js @@ -8,7 +8,7 @@ import { FormattedMessage } from 'react-intl'; import ResultsTableRow from './ResultsTableRow'; import LoadingResultsTableRow from './LoadingResultsTableRow'; import NoResultsAvailableRow from './NoResultsAvailableRow'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import { LocalizedExerciseName } from '../../helpers/LocalizedNames'; import styles from './ResultsTable.less'; diff --git a/src/components/Groups/SupervisorsView/SupervisorsView.js b/src/components/Groups/SupervisorsView/SupervisorsView.js index b99fabc77..7d19b0047 100644 --- a/src/components/Groups/SupervisorsView/SupervisorsView.js +++ b/src/components/Groups/SupervisorsView/SupervisorsView.js @@ -5,20 +5,21 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Row, Col } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; -import ResourceRenderer from '../../helpers/ResourceRenderer'; -import Box from '../../widgets/Box'; -import AddStudent from '../AddStudent'; -import SearchExercise from '../SearchExercise'; -import AdminAssignmentsTable from '../../Assignments/AdminAssignmentsTable'; +import DeleteExerciseButtonContainer from '../../../containers/DeleteExerciseButtonContainer'; import ExercisesSimpleList from '../../../components/Exercises/ExercisesSimpleList'; import ResultsTableContainer from '../../../containers/ResultsTableContainer'; import Button from '../../../components/widgets/FlatButton'; -import { AddIcon, EditIcon, DeleteIcon } from '../../../components/icons'; +import { AddIcon, EditIcon } from '../../../components/icons'; import AssignExerciseButton from '../../../components/buttons/AssignExerciseButton'; + +import ResourceRenderer from '../../helpers/ResourceRenderer'; +import Box from '../../widgets/Box'; +import AddStudent from '../AddStudent'; +import SearchExercise from '../SearchExercise'; +import AdminAssignmentsTable from '../../Assignments/AdminAssignmentsTable'; import { deleteExercise } from '../../../redux/modules/exercises'; -import Confirm from '../../../components/forms/Confirm'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import { getLocalizedName } from '../../../helpers/getLocalizedData'; const SupervisorsView = ({ @@ -30,7 +31,11 @@ const SupervisorsView = ({ deleteExercise, users, publicAssignments, - links: { EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY }, + links: { + EXERCISE_EDIT_URI_FACTORY, + EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, + EXERCISE_EDIT_LIMITS_URI_FACTORY + }, intl: { locale } }) =>
@@ -147,52 +152,43 @@ const SupervisorsView = ({ isBroken={isBroken} assignExercise={() => assignExercise(exerciseId)} /> + - - - deleteExercise(exerciseId)} - question={ - - } + - - + + +
} />} diff --git a/src/components/Instances/InstancesTable/InstancesTable.js b/src/components/Instances/InstancesTable/InstancesTable.js index 02ef73151..37f610069 100644 --- a/src/components/Instances/InstancesTable/InstancesTable.js +++ b/src/components/Instances/InstancesTable/InstancesTable.js @@ -6,7 +6,7 @@ import { Link } from 'react-router'; import { MaybeSucceededIcon } from '../../icons'; import UsersNameContainer from '../../../containers/UsersNameContainer'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const InstancesTable = ({ instances, links: { INSTANCE_URI_FACTORY }, intl }) => diff --git a/src/components/Pipelines/PipelinesListItem/PipelinesListItem.js b/src/components/Pipelines/PipelinesListItem/PipelinesListItem.js index b0e048c26..3a3673125 100644 --- a/src/components/Pipelines/PipelinesListItem/PipelinesListItem.js +++ b/src/components/Pipelines/PipelinesListItem/PipelinesListItem.js @@ -6,7 +6,7 @@ import ExercisesNameContainer from '../../../containers/ExercisesNameContainer'; import { Link } from 'react-router'; import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const PipelinesListItem = ({ id, diff --git a/src/components/Pipelines/PipelinesSimpleListItem/PipelinesSimpleListItem.js b/src/components/Pipelines/PipelinesSimpleListItem/PipelinesSimpleListItem.js index 48fb6bf79..043d9750f 100644 --- a/src/components/Pipelines/PipelinesSimpleListItem/PipelinesSimpleListItem.js +++ b/src/components/Pipelines/PipelinesSimpleListItem/PipelinesSimpleListItem.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import UsersNameContainer from '../../../containers/UsersNameContainer'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const PipelinesSimpleListItem = ({ id, diff --git a/src/components/ReferenceSolutions/ReferenceSolutionEvaluations/ReferenceSolutionEvaluations.js b/src/components/ReferenceSolutions/ReferenceSolutionEvaluations/ReferenceSolutionEvaluations.js index a2de2ca12..6dc8ca922 100644 --- a/src/components/ReferenceSolutions/ReferenceSolutionEvaluations/ReferenceSolutionEvaluations.js +++ b/src/components/ReferenceSolutions/ReferenceSolutionEvaluations/ReferenceSolutionEvaluations.js @@ -5,7 +5,7 @@ import { Link } from 'react-router'; import Box from '../../widgets/Box'; import EvaluationTable from '../EvaluationTable'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const ReferenceSolutionEvaluations = ({ evaluations, diff --git a/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js index 3effa18b8..3f5f0581e 100644 --- a/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js +++ b/src/components/SubmissionFailures/FailuresListItem/FailuresListItem.js @@ -4,7 +4,7 @@ import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; import Icon from 'react-fontawesome'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { Link } from 'react-router'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const FailuresListItem = ({ id, diff --git a/src/components/Submissions/SubmissionEvaluations/SubmissionEvaluations.js b/src/components/Submissions/SubmissionEvaluations/SubmissionEvaluations.js index 32f13047e..9a8bc3907 100644 --- a/src/components/Submissions/SubmissionEvaluations/SubmissionEvaluations.js +++ b/src/components/Submissions/SubmissionEvaluations/SubmissionEvaluations.js @@ -5,7 +5,7 @@ import Button from '../../widgets/FlatButton'; import Box from '../../widgets/Box'; import EvaluationTable from '../../ReferenceSolutions/EvaluationTable'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const SubmissionEvaluations = ({ evaluations, diff --git a/src/components/Submissions/SubmissionStatus/SubmissionStatus.js b/src/components/Submissions/SubmissionStatus/SubmissionStatus.js index 2e865c4d8..6c2a395ed 100644 --- a/src/components/Submissions/SubmissionStatus/SubmissionStatus.js +++ b/src/components/Submissions/SubmissionStatus/SubmissionStatus.js @@ -6,7 +6,7 @@ import { FormattedDate, FormattedTime, FormattedMessage } from 'react-intl'; import Box from '../../widgets/Box'; import AssignmentStatusIcon from '../../Assignments/Assignment/AssignmentStatusIcon'; import UsersNameContainer from '../../../containers/UsersNameContainer'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; const SubmissionStatus = ({ evaluationStatus, diff --git a/src/components/Users/UsersName/UsersName.js b/src/components/Users/UsersName/UsersName.js index d07d22816..c43933484 100644 --- a/src/components/Users/UsersName/UsersName.js +++ b/src/components/Users/UsersName/UsersName.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Link } from 'react-router'; import Avatar from '../../widgets/Avatar'; import NotVerified from './NotVerified'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import styles from './usersName.less'; diff --git a/src/components/Users/UsersName/usersName.less b/src/components/Users/UsersName/usersName.less index 2d42a7c40..3bdfee0d6 100644 --- a/src/components/Users/UsersName/usersName.less +++ b/src/components/Users/UsersName/usersName.less @@ -1,6 +1,7 @@ .wrapper { overflow: hidden; display: inline-block; + white-space: nowrap; } .avatar { diff --git a/src/components/buttons/DeleteButton/ConfirmDeleteButton.js b/src/components/buttons/DeleteButton/ConfirmDeleteButton.js index 5517b5d8f..71792a98e 100644 --- a/src/components/buttons/DeleteButton/ConfirmDeleteButton.js +++ b/src/components/buttons/DeleteButton/ConfirmDeleteButton.js @@ -26,7 +26,7 @@ const ConfirmDeleteButton = ({ {...props} > {' '} - + ; diff --git a/src/components/buttons/DeleteButton/DeletedButton.js b/src/components/buttons/DeleteButton/DeletedButton.js index 03daef6b8..9be11e6f1 100644 --- a/src/components/buttons/DeleteButton/DeletedButton.js +++ b/src/components/buttons/DeleteButton/DeletedButton.js @@ -3,12 +3,10 @@ import { FormattedMessage } from 'react-intl'; import Button from '../../widgets/FlatButton'; import { SuccessIcon } from '../../icons'; -const DeletedGroupButton = props => ( +const DeletedGroupButton = props => -); + {' '} + + ; export default DeletedGroupButton; diff --git a/src/components/buttons/DeleteButton/DeletingButton.js b/src/components/buttons/DeleteButton/DeletingButton.js index 57a3bc841..88a6afe74 100644 --- a/src/components/buttons/DeleteButton/DeletingButton.js +++ b/src/components/buttons/DeleteButton/DeletingButton.js @@ -3,15 +3,10 @@ import { FormattedMessage } from 'react-intl'; import Button from '../../widgets/FlatButton'; import { LoadingIcon } from '../../icons'; -const DeletingGroupButton = props => ( +const DeletingGroupButton = props => -); + {' '} + + ; export default DeletingGroupButton; diff --git a/src/components/buttons/DeleteButton/DeletingFailedButton.js b/src/components/buttons/DeleteButton/DeletingFailedButton.js index 4ceaac315..0b798f20a 100644 --- a/src/components/buttons/DeleteButton/DeletingFailedButton.js +++ b/src/components/buttons/DeleteButton/DeletingFailedButton.js @@ -14,8 +14,8 @@ const DeletingFailedButton = ({ onClick, ...props }) => > {' '} ; diff --git a/src/components/forms/EditAssignmentForm/EditAssignmentForm.js b/src/components/forms/EditAssignmentForm/EditAssignmentForm.js index 7a7c803d6..a4e8e5275 100644 --- a/src/components/forms/EditAssignmentForm/EditAssignmentForm.js +++ b/src/components/forms/EditAssignmentForm/EditAssignmentForm.js @@ -18,8 +18,8 @@ const EditAssignmentForm = ({ anyTouched, submitting, handleSubmit, - submitFailed: hasFailed, - submitSucceeded: hasSucceeded, + submitFailed, + submitSucceeded, asyncValidating, invalid, firstDeadline, @@ -35,7 +35,7 @@ const EditAssignmentForm = ({ values={{ name: }} /> } - successful={hasSucceeded} + successful={submitSucceeded} dirty={anyTouched} unlimitedHeight footer={ @@ -45,8 +45,8 @@ const EditAssignmentForm = ({ invalid={invalid} submitting={submitting} dirty={anyTouched} - hasSucceeded={hasSucceeded} - hasFailed={hasFailed} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} handleSubmit={handleSubmit} asyncValidating={asyncValidating} messages={{ @@ -73,7 +73,7 @@ const EditAssignmentForm = ({ } > - {hasFailed && + {submitFailed && - {hasFailed && + {submitFailed && - {hasFailed && + {submitFailed &&
- {hasFailed && + {submitFailed && }} /> } - succeeded={hasSucceeded} + succeeded={submitSucceeded} dirty={anyTouched} footer={
@@ -73,8 +70,8 @@ const EditExerciseForm = ({ invalid={invalid} submitting={submitting} dirty={anyTouched} - hasSucceeded={hasSucceeded} - hasFailed={hasFailed} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} handleSubmit={handleSubmit} asyncValidating={asyncValidating} messages={{ @@ -104,21 +101,10 @@ const EditExerciseForm = ({ ) }} /> - - -
} > - {hasFailed && + {submitFailed && + + } + type={submitSucceeded ? 'success' : undefined} + footer={ +
+ {dirty && + !submitting && + !submitSucceeded && + + {' '} + } + + ), + submitting: ( + + ), + success: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + ({ key: hwg.id, name: hwg.name })) + .sort((a, b) => a.key.localeCompare(b.key)) // intentionally no locale (key is our identifier) + } + addEmptyOption={addEmptyOption} + label={ + + } + /> +
; + +EditHardwareGroupForm.propTypes = { + handleSubmit: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + submitFailed: PropTypes.bool, + dirty: PropTypes.bool, + submitSucceeded: PropTypes.bool, + submitting: PropTypes.bool, + invalid: PropTypes.bool, + reset: PropTypes.func.isRequired, + hardwareGroups: PropTypes.array.isRequired, + addEmptyOption: PropTypes.bool +}; + +const validate = ({ bonusPoints }) => { + const errors = {}; + if (!isInt(String(bonusPoints))) { + errors['bonusPoints'] = ( + + ); + } + + return errors; +}; + +export default reduxForm({ + form: 'editHardwareGroup', + enableReinitialize: true, + keepDirtyOnReinitialize: false, + validate +})(EditHardwareGroupForm); diff --git a/src/components/forms/EditHardwareGroupForm/index.js b/src/components/forms/EditHardwareGroupForm/index.js new file mode 100644 index 000000000..96a5c24cc --- /dev/null +++ b/src/components/forms/EditHardwareGroupForm/index.js @@ -0,0 +1 @@ +export default from './EditHardwareGroupForm'; diff --git a/src/components/forms/EditHardwareGroupLimits/EditHardwareGroupLimits.js b/src/components/forms/EditHardwareGroupLimits/EditHardwareGroupLimits.js deleted file mode 100644 index 0b6274e9b..000000000 --- a/src/components/forms/EditHardwareGroupLimits/EditHardwareGroupLimits.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { TabbedArrayField } from '../Fields'; - -import HardwareGroupFields from './HardwareGroupFields'; - -const EditHardwareGroupLimits = ({ - limits, - referenceSolutionsEvaluations, - ...props -}) => ( - limits[i].hardwareGroup} - id="edit-hardware-group-limits" - add={false} - remove={false} - referenceSolutionsEvaluations={referenceSolutionsEvaluations} - ContentComponent={HardwareGroupFields} - /> -); - -EditHardwareGroupLimits.propTypes = { - limits: PropTypes.array, - referenceSolutionsEvaluations: PropTypes.object -}; - -export default EditHardwareGroupLimits; diff --git a/src/components/forms/EditHardwareGroupLimits/HardwareGroupFields.js b/src/components/forms/EditHardwareGroupLimits/HardwareGroupFields.js deleted file mode 100644 index 3ba67a3e3..000000000 --- a/src/components/forms/EditHardwareGroupLimits/HardwareGroupFields.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { Field } from 'redux-form'; -import { Row, Col, HelpBlock } from 'react-bootstrap'; -import { KiloBytesTextField, SecondsTextField } from '../Fields'; -import ReferenceSolutionsEvaluationsResults from '../../ReferenceSolutions/ReferenceSolutionsEvaluationsResults'; - -const sortTests = tests => { - return tests.sort((a, b) => a.localeCompare(b)); -}; - -const HardwareGroupFields = ({ - prefix, - i, - limits, - referenceSolutionsEvaluations -}) => { - const { hardwareGroup, tests } = limits[i]; - const referenceSolutionsEvaluationsResults = - referenceSolutionsEvaluations[hardwareGroup]; - return ( - - {sortTests(Object.keys(tests)).map((testName, j) => -
-

- {' '} - {testName} -

- {Object.keys(tests[testName]).map((taskId, k) => -
- - } - /> - - } - /> - - {referenceSolutionsEvaluationsResults && - } - - {!referenceSolutionsEvaluationsResults && - - - } -
- )} - - )} - - ); -}; - -HardwareGroupFields.propTypes = { - prefix: PropTypes.string.isRequired, - i: PropTypes.number, - limits: PropTypes.array.isRequired, - referenceSolutionsEvaluations: PropTypes.object -}; - -export default HardwareGroupFields; diff --git a/src/components/forms/EditHardwareGroupLimits/index.js b/src/components/forms/EditHardwareGroupLimits/index.js deleted file mode 100644 index 03d9f1956..000000000 --- a/src/components/forms/EditHardwareGroupLimits/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './EditHardwareGroupLimits'; diff --git a/src/components/forms/EditLimits/EditEnvironmentLimitsFields.js b/src/components/forms/EditLimits/EditEnvironmentLimitsFields.js deleted file mode 100644 index 5bd091c04..000000000 --- a/src/components/forms/EditLimits/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/EditLimits/EditEnvironmentLimitsForm.js b/src/components/forms/EditLimits/EditEnvironmentLimitsForm.js deleted file mode 100644 index b0e33d83c..000000000 --- a/src/components/forms/EditLimits/EditEnvironmentLimitsForm.js +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { reduxForm } from 'redux-form'; -import { Label } from 'react-bootstrap'; -import Icon from 'react-fontawesome'; - -import { LimitsField } from '../Fields'; -import SubmitButton from '../SubmitButton'; - -const EditEnvironmentLimitsForm = ({ - config, - envName, - onSubmit, - anyTouched, - submitting, - handleSubmit, - submitFailed: hasFailed, - submitSucceeded: hasSucceeded, - invalid, - asyncValidating, - getBoxesWithLimits, - ...props -}) => -
- {config.tests.map(test => -
-

- {test.name} -

- {test.pipelines.length === 0 - ? - {' '} - - - : test.pipelines.map(pipeline => -
-
- {pipeline.name} {' '} - -
- - {getBoxesWithLimits(pipeline.name).length === 0 - ? - {' '} - - - : getBoxesWithLimits(pipeline.name).map(box => -
-
- {box.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, - getBoxesWithLimits: PropTypes.func.isRequired, - asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) -}; - -const validate = ({ limits }) => { - const errors = {}; - - for (let test of Object.keys(limits)) { - const testErrors = {}; - for (let pipeline of Object.keys(limits[test])) { - const pipelineErrors = {}; - for (let box of Object.keys(limits[test][pipeline])) { - const boxErrors = {}; - const fields = limits[test][pipeline][box]; - - if (!fields['memory'] || fields['memory'].length === 0) { - boxErrors['memory'] = ( - - ); - } else if ( - Number(fields['memory']).toString() !== fields['memory'] || - Number(fields['memory']) <= 0 - ) { - boxErrors['memory'] = ( - - ); - } - - if (boxErrors.length > 0) { - pipelineErrors[box] = boxErrors; - } - } - - if (pipelineErrors.length > 0) { - testErrors[pipeline] = pipelineErrors; - } - } - - if (testErrors.length > 0) { - errors[test] = testErrors; - } - } - - return errors; -}; - -export default reduxForm({ - form: 'editLimits', - validate -})(EditEnvironmentLimitsForm); diff --git a/src/components/forms/EditLimits/EditLimits.js b/src/components/forms/EditLimits/EditLimits.js deleted file mode 100644 index e7404817f..000000000 --- a/src/components/forms/EditLimits/EditLimits.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Row, Col, Label } from 'react-bootstrap'; - -import EditEnvironmentLimitsForm from './EditEnvironmentLimitsForm'; -import ResourceRenderer from '../../helpers/ResourceRenderer'; - -import styles from './styles.less'; - -const EditLimits = ({ - environments = [], - editLimits, - limits, - config, - ...props -}) => - - {environments.map(({ id, name, platform, description }, i) => - -
-

- {name} -

-

- {description} -

- - {limits => - forEnv.name === id)} - initialValues={{ limits }} - form={`editEnvironmentLimits-${id}`} - onSubmit={editLimits(id)} - />} - -
- - )} - ; - -EditLimits.propTypes = { - config: PropTypes.array.isRequired, - environments: PropTypes.array, - editLimits: PropTypes.func.isRequired, - limits: PropTypes.func.isRequired -}; - -export default EditLimits; diff --git a/src/components/forms/EditLimits/index.js b/src/components/forms/EditLimits/index.js deleted file mode 100644 index beadc332f..000000000 --- a/src/components/forms/EditLimits/index.js +++ /dev/null @@ -1 +0,0 @@ -export default from './EditLimits'; diff --git a/src/components/forms/EditLimits/styles.less b/src/components/forms/EditLimits/styles.less deleted file mode 100644 index 84a884ca6..000000000 --- a/src/components/forms/EditLimits/styles.less +++ /dev/null @@ -1,10 +0,0 @@ -.oddLimitsCol { - padding: 20px; - margin: 10px; - background: #fafafa; -} - -.evenLimitsCol { - padding: 20px; - margin: 10px; -} diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditLimitsForm/EditLimitsForm.js similarity index 73% rename from src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js rename to src/components/forms/EditLimitsForm/EditLimitsForm.js index c110d07fa..b2273d362 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditLimitsForm/EditLimitsForm.js @@ -5,20 +5,17 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { reduxForm, Field } from 'redux-form'; import Icon from 'react-fontawesome'; -import { EditSimpleLimitsField, CheckboxField } from '../Fields'; +import { EditLimitsField, CheckboxField } 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 { encodeId, encodeNumId } from '../../../helpers/common'; +import { validateLimitsTimeTotals } from '../../../helpers/exerciseLimits'; import styles from './styles.less'; -class EditSimpleLimitsForm extends Component { +class EditLimitsForm extends Component { render() { const { environments, @@ -35,6 +32,7 @@ class EditSimpleLimitsForm extends Component { invalid, intl: { locale } } = this.props; + return (   {' '} } ), submitting: ( ), success: ( ), validating: ( ) @@ -105,7 +103,7 @@ class EditSimpleLimitsForm extends Component { {submitFailed && } @@ -118,7 +116,7 @@ class EditSimpleLimitsForm extends Component { onOff label={ } @@ -130,7 +128,7 @@ class EditSimpleLimitsForm extends Component {

@@ -162,9 +160,7 @@ class EditSimpleLimitsForm extends Component { {environments.map(environment => { const id = - encodeTestId(test.id) + - '.' + - encodeEnvironmentId(environment.id); + encodeNumId(test.id) + '.' + encodeId(environment.id); return (
= 3 ? 12 : 6} md={12}> +
1 ? styles.colSeparator : '' } > - { +const validate = ({ limits }, { constraints }) => { 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]['time']) { - const val = Number(limits[test][env]['time']); - if (!Number.isNaN(val) && val > 0) { - sums[env] = (sums[env] || 0) + val; - } - } - }) - ); + const limitsErrors = validateLimitsTimeTotals(limits, constraints.totalTime); - // 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] = { - time: ( - - ) - }; - } + Object.keys(limitsErrors).forEach(test => { + Object.keys(limitsErrors[test]).forEach(env => { + limitsErrors[test][env] = { + time: ( + + ) + }; }); - if (Object.keys(testsErrors).length > 0) { - limitsErrors[test] = testsErrors; - } }); if (Object.keys(limitsErrors).length > 0) { errors['limits'] = limitsErrors; @@ -271,7 +243,7 @@ const validate = ({ limits }) => { }; export default reduxForm({ - form: 'editSimpleLimits', + form: 'editLimits', enableReinitialize: true, keepDirtyOnReinitialize: false, immutableProps: [ @@ -283,4 +255,4 @@ export default reduxForm({ 'handleSubmit' ], validate -})(injectIntl(EditSimpleLimitsForm)); +})(injectIntl(EditLimitsForm)); diff --git a/src/components/forms/EditLimitsForm/index.js b/src/components/forms/EditLimitsForm/index.js new file mode 100644 index 000000000..aefd774f3 --- /dev/null +++ b/src/components/forms/EditLimitsForm/index.js @@ -0,0 +1 @@ +export default from './EditLimitsForm'; diff --git a/src/components/forms/EditSimpleLimitsForm/styles.less b/src/components/forms/EditLimitsForm/styles.less similarity index 100% rename from src/components/forms/EditSimpleLimitsForm/styles.less rename to src/components/forms/EditLimitsForm/styles.less diff --git a/src/components/forms/EditPipelineForm/EditPipelineForm.js b/src/components/forms/EditPipelineForm/EditPipelineForm.js index 2029eed7d..88258625c 100644 --- a/src/components/forms/EditPipelineForm/EditPipelineForm.js +++ b/src/components/forms/EditPipelineForm/EditPipelineForm.js @@ -38,8 +38,8 @@ class EditPipelineForm extends Component { anyTouched, submitting, handleSubmit, - submitFailed: hasFailed, - submitSucceeded: hasSucceeded, + submitFailed, + submitSucceeded, variables = [], invalid, asyncValidating, @@ -54,7 +54,7 @@ class EditPipelineForm extends Component { values={{ name: pipeline.name }} /> } - succeeded={hasSucceeded} + succeeded={submitSucceeded} dirty={anyTouched} footer={
@@ -63,8 +63,8 @@ class EditPipelineForm extends Component { invalid={invalid} submitting={submitting} dirty={anyTouched} - hasSucceeded={hasSucceeded} - hasFailed={hasFailed} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} handleSubmit={handleSubmit} asyncValidating={asyncValidating} messages={{ @@ -97,7 +97,7 @@ class EditPipelineForm extends Component {
} > - {hasFailed && + {submitFailed &&
- {hasFailed && + {submitFailed && - {hasFailed && + {submitFailed && {dirty && !submitting && - !hasSucceeded && + !submitSucceeded &&
+ + + } + 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 && + + } + > + + + + } + > + + + + + } +
+ +
+
+ ); +}; + +EditLimitsField.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 EditLimitsField; diff --git a/src/components/forms/Fields/EditSimpleLimitsField.less b/src/components/forms/Fields/EditLimitsField.less similarity index 100% rename from src/components/forms/Fields/EditSimpleLimitsField.less rename to src/components/forms/Fields/EditLimitsField.less diff --git a/src/components/forms/Fields/EditSimpleLimitsField.js b/src/components/forms/Fields/EditSimpleLimitsField.js deleted file mode 100644 index f630447b9..000000000 --- a/src/components/forms/Fields/EditSimpleLimitsField.js +++ /dev/null @@ -1,287 +0,0 @@ -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/KiloBytesTextField.js b/src/components/forms/Fields/KiloBytesTextField.js deleted file mode 100644 index d1d0f6195..000000000 --- a/src/components/forms/Fields/KiloBytesTextField.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { HelpBlock } from 'react-bootstrap'; -import { FormattedMessage } from 'react-intl'; - -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 }) => -
- - - {' '} - {prettyPrintBytes(Number(input.value) * 1024)} - -
; - -KiloBytesTextField.propTypes = { - input: PropTypes.shape({ - value: PropTypes.any.isRequired - }).isRequired -}; - -export default KiloBytesTextField; diff --git a/src/components/forms/Fields/LimitsField.js b/src/components/forms/Fields/LimitsField.js deleted file mode 100644 index 4cce55869..000000000 --- a/src/components/forms/Fields/LimitsField.js +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Field } from 'redux-form'; -import { FormattedMessage } from 'react-intl'; -import Icon from 'react-fontawesome'; - -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, - id, - setHorizontally, - setVertically, - setAll, - ...props -}) => -
- - } - {...props} - /> - - } - {...props} - /> - -

- - - - - - - - } - > - - - - -

-
; - -LimitsField.propTypes = { - label: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ type: PropTypes.oneOf([FormattedMessage]) }), - PropTypes.element - ]).isRequired, - id: PropTypes.string.isRequired, - setHorizontally: PropTypes.func.isRequired, - setVertically: PropTypes.func.isRequired, - setAll: PropTypes.func.isRequired, - prefix: PropTypes.string.isRequired, - ports: PropTypes.array -}; - -export default LimitsField; diff --git a/src/components/forms/Fields/LimitsValueField.js b/src/components/forms/Fields/LimitsValueField.js index 1b915b807..daea36721 100644 --- a/src/components/forms/Fields/LimitsValueField.js +++ b/src/components/forms/Fields/LimitsValueField.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { TextField } from '../Fields'; -import styles from './EditSimpleLimitsField.less'; +import styles from './EditLimitsField.less'; const LimitsValueField = ({ input, prettyPrint, ...props }) => diff --git a/src/components/forms/Fields/SecondsTextField.js b/src/components/forms/Fields/SecondsTextField.js deleted file mode 100644 index a033c045f..000000000 --- a/src/components/forms/Fields/SecondsTextField.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import prettyMs from 'pretty-ms'; -import TextField from './TextField'; -import { HelpBlock } from 'react-bootstrap'; -import { FormattedMessage } from 'react-intl'; - -// !!! this component is no longer used in EditSimpleLimits, but it so may happen it will be recycled for the complex edit form... -const SecondsTextField = ({ input, ...props }) => -
- - {!props.meta.error && - !isNaN(Number(input.value)) && - false && - - {' '} - {prettyMs(Number(input.value) * 1000)} - } -
; - -SecondsTextField.propTypes = { - input: PropTypes.shape({ - value: PropTypes.any.isRequired - }).isRequired, - meta: PropTypes.shape({ - error: PropTypes.any - }).isRequired -}; - -export default SecondsTextField; diff --git a/src/components/forms/Fields/index.js b/src/components/forms/Fields/index.js index 4981a83c6..0c723dc11 100644 --- a/src/components/forms/Fields/index.js +++ b/src/components/forms/Fields/index.js @@ -2,18 +2,15 @@ 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 EditLimitsField } from './EditLimitsField'; export { default as LimitsValueField } from './LimitsValueField'; -export { default as LimitsField } from './LimitsField'; export { default as PasswordField } from './PasswordField'; export { default as PasswordStrength } from './PasswordStrength'; -export { default as KiloBytesTextField } from './KiloBytesTextField'; export { default as LanguageSelectField } from './LanguageSelectField'; export { default as PipelineField } from './PipelineField'; export { default as PipelineVariablesField } from './PipelineVariablesField'; export { default as PortField } from './PortField'; export { default as PortsField } from './PortsField'; -export { default as SecondsTextField } from './SecondsTextField'; export { default as SelectField } from './SelectField'; export { default as SingleUploadField } from './SingleUploadField'; export { default as SourceCodeField } from './SourceCodeField'; diff --git a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js index 20119434d..7c7dcef5c 100644 --- a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js +++ b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js @@ -15,7 +15,7 @@ import { getFork } from '../../../redux/selectors/exercises'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import { getLocalizedName } from '../../../helpers/getLocalizedData'; -import withLinks from '../../../hoc/withLinks'; +import withLinks from '../../../helpers/withLinks'; import './ForkExerciseForm.css'; @@ -37,8 +37,8 @@ class ForkExerciseForm extends Component { anyTouched, submitting, handleSubmit, - hasFailed = false, - hasSucceeded = false, + submitFailed, + submitSucceeded, invalid, groups, intl: { locale } @@ -63,7 +63,7 @@ class ForkExerciseForm extends Component { default: return (
- {hasFailed && + {submitFailed && - {hasFailed && + {submitFailed && } - succeeded={hasSucceeded} + succeeded={submitSucceeded} dirty={anyTouched} footer={
@@ -37,8 +37,8 @@ const SisBindGroupForm = ({ invalid={invalid} submitting={submitting} dirty={anyTouched} - hasSucceeded={hasSucceeded} - hasFailed={hasFailed} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} handleSubmit={handleSubmit} messages={{ submit: ( @@ -64,7 +64,7 @@ const SisBindGroupForm = ({
} > - {hasFailed && + {submitFailed && } - succeeded={hasSucceeded} + succeeded={submitSucceeded} dirty={anyTouched} footer={
@@ -40,8 +40,8 @@ class SisCreateGroupForm extends Component { invalid={invalid} submitting={submitting} dirty={anyTouched} - hasSucceeded={hasSucceeded} - hasFailed={hasFailed} + hasSucceeded={submitSucceeded} + hasFailed={submitFailed} handleSubmit={handleSubmit} messages={{ submit: ( @@ -67,7 +67,7 @@ class SisCreateGroupForm extends Component {
} > - {hasFailed && + {submitFailed && { diff --git a/src/containers/BadgeContainer/BadgeContainer.js b/src/containers/BadgeContainer/BadgeContainer.js index cb98e3d0a..118d8fa70 100644 --- a/src/containers/BadgeContainer/BadgeContainer.js +++ b/src/containers/BadgeContainer/BadgeContainer.js @@ -13,7 +13,7 @@ import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import { loggedInUserSelector } from '../../redux/selectors/users'; import { accessTokenExpiration } from '../../redux/selectors/auth'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; const BadgeContainer = ({ user, diff --git a/src/containers/CAS/AuthenticationButtonContainer.js b/src/containers/CAS/AuthenticationButtonContainer.js index 9756b6651..1e4e1eda0 100644 --- a/src/containers/CAS/AuthenticationButtonContainer.js +++ b/src/containers/CAS/AuthenticationButtonContainer.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Authenticate, LoginFailed } from '../../components/buttons/CAS'; import { openCASWindow } from '../../helpers/cas'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import { absolute } from '../../links'; class AuthenticationButtonContainer extends Component { diff --git a/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js b/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js index 9f6423693..9bac27f98 100644 --- a/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js +++ b/src/containers/ResubmitSolutionContainer/ResubmitSolutionContainer.js @@ -13,7 +13,7 @@ import { getSubmissionId } from '../../redux/selectors/submission'; import EvaluationProgressContainer from '../EvaluationProgressContainer'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import { fetchSubmissionEvaluationsForSolution } from '../../redux/modules/submissionEvaluations'; const ResubmitSolutionContainer = ({ diff --git a/src/containers/SisIntegrationContainer/SisIntegrationContainer.js b/src/containers/SisIntegrationContainer/SisIntegrationContainer.js index 39b9cf0cf..c0244f5cf 100644 --- a/src/containers/SisIntegrationContainer/SisIntegrationContainer.js +++ b/src/containers/SisIntegrationContainer/SisIntegrationContainer.js @@ -21,7 +21,7 @@ import LeaveJoinGroupButtonContainer from '../LeaveJoinGroupButtonContainer'; import { getGroupCanonicalLocalizedName } from '../../helpers/getLocalizedData'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class SisIntegrationContainer extends Component { componentDidMount() { diff --git a/src/containers/SisSupervisorGroupsContainer/SisSupervisorGroupsContainer.js b/src/containers/SisSupervisorGroupsContainer/SisSupervisorGroupsContainer.js index 815657e2f..5b40ada3b 100644 --- a/src/containers/SisSupervisorGroupsContainer/SisSupervisorGroupsContainer.js +++ b/src/containers/SisSupervisorGroupsContainer/SisSupervisorGroupsContainer.js @@ -29,7 +29,7 @@ import SisCreateGroupForm from '../../components/forms/SisCreateGroupForm'; import SisBindGroupForm from '../../components/forms/SisBindGroupForm'; import { getGroupCanonicalLocalizedName } from '../../helpers/getLocalizedData'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import './SisSupervisorGroupsContainer.css'; const days = { diff --git a/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js b/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js index 1fd98a0c8..c20258a4a 100644 --- a/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js +++ b/src/containers/SubmitSolutionContainer/SubmitSolutionContainer.js @@ -25,7 +25,7 @@ import { cancel, changeNote } from '../../redux/modules/submission'; import { reset as resetUpload } from '../../redux/modules/upload'; import { evaluateReferenceSolution } from '../../redux/modules/referenceSolutions'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class SubmitSolutionContainer extends Component { state = { diff --git a/src/containers/UsersNameContainer/UsersNameContainer.js b/src/containers/UsersNameContainer/UsersNameContainer.js index ab27d5ed1..721f187b4 100644 --- a/src/containers/UsersNameContainer/UsersNameContainer.js +++ b/src/containers/UsersNameContainer/UsersNameContainer.js @@ -41,7 +41,7 @@ class UsersNameContainer extends Component { > {user => isSimple - ? + ? {user.name.firstName} {user.name.lastName} : {}; +export const identity = x => x; + +// Safe getter to traverse compex object/array structures. +export const safeGet = (obj, path, def = undefined) => { + if (!Array.isArray(path)) { + path = [path]; + } + path.forEach(step => { + obj = obj && (typeof step === 'function' ? obj.find(step) : obj[step]); + }); + return obj === undefined ? def : obj; +}; + +export const encodeId = id => { + return 'BID' + btoa(id); +}; + +export const encodeNumId = id => { + return 'ID' + id; +}; diff --git a/src/helpers/exerciseLimits.js b/src/helpers/exerciseLimits.js new file mode 100644 index 000000000..371021443 --- /dev/null +++ b/src/helpers/exerciseLimits.js @@ -0,0 +1,236 @@ +import { defaultMemoize } from 'reselect'; + +import { encodeId, encodeNumId, safeGet } from '../helpers/common'; +import { endpointDisguisedAsIdFactory } from '../redux/modules/limits'; + +/* + * Memory and Time limits + */ +export const getLimitsInitValues = defaultMemoize( + (limits, tests, environments, exerciseId, hwGroup) => { + let res = {}; + let wallTimeCount = 0; + let cpuTimeCount = 0; + + tests.forEach(test => { + const testEnc = encodeNumId(test.id); + res[testEnc] = {}; + environments.forEach(environment => { + const envId = encodeId(environment.id); + let lim = limits.getIn([ + endpointDisguisedAsIdFactory({ + exerciseId, + hwGroup, + 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 + }; + }); + }); + + // 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'; + } + } + + return { + limits: res, + preciseTime + }; + } +); + +const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { + let res = { + memory + }; + res[timeField] = time; + return 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 transformLimitsValues = (formData, tests, runtimeEnvironments) => + runtimeEnvironments.map(environment => { + const envId = encodeId(environment.id); + const data = { + limits: tests.reduce((acc, test) => { + acc[test.id] = transformLimitsObject( + formData.limits[encodeNumId(test.id)][envId], + formData.preciseTime ? 'cpu-time' : 'wall-time' + ); + return acc; + }, {}) + }; + return { id: environment.id, data }; + }); + +/** + * Validation + */ + +const LIMITS_CONSTRAINTS_DEFAULTS = { + memory: 1024 * 1024, + cpuTimePerTest: 60, // 1 minute + cpuTimePerExercise: 300, // 5 minutes + wallTimePerTest: 60, + wallTimePerExercise: 300 +}; + +const combineHardwareGroupLimitsConstraints = exerciseHwGroups => { + // Reduce all hwGroup metadata using min function + const res = {}; + exerciseHwGroups.forEach(({ metadata }) => + Object.keys(LIMITS_CONSTRAINTS_DEFAULTS).forEach(key => { + if (metadata[key]) { + res[key] = + res[key] !== undefined + ? Math.min(res[key], metadata[key]) + : metadata[key]; + } + }) + ); + + // Make sure all keys exists (use defaults for missing ones). + Object.keys(LIMITS_CONSTRAINTS_DEFAULTS).forEach(key => { + res[key] = res[key] || LIMITS_CONSTRAINTS_DEFAULTS[key]; + }); + + return res; +}; + +// Compute limit constraints from hwGroup metadata +export const getLimitsConstraints = defaultMemoize( + (exerciseHwGroups, preciseTime) => { + const res = combineHardwareGroupLimitsConstraints(exerciseHwGroups); + return { + memory: { min: 128, max: res.memory }, + time: { + min: 0.1, + max: res[preciseTime ? 'cpuTimePerTest' : 'wallTimePerTest'] + }, + totalTime: { + min: 0.1, + max: res[preciseTime ? 'cpuTimePerExercise' : 'wallTimePerExercise'] + } + }; + } +); + +// Compute complete list for visualization of a single +export const getLimitsConstraintsOfSingleGroup = defaultMemoize( + exerciseHwGroup => { + const res = combineHardwareGroupLimitsConstraints([exerciseHwGroup]); + for (const key in res) { + res[key] = { + min: key === 'memory' ? 128 : 0.1, + max: res[key] + }; + } + return res; + } +); + +export const validateLimitsField = (value, range) => { + const num = Number(value); + if (Number.isNaN(num)) { + return num; + } + if (range && (num < range.min || num > range.max)) { + return false; + } + return num; +}; + +export const validateLimitsTimeTotals = (limits, range) => { + // Compute sum of times for each environment. + let sums = {}; + Object.keys(limits).forEach(test => + Object.keys(limits[test]).forEach(env => { + if (limits[test][env]['time']) { + const val = Number(limits[test][env]['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] > range.max || sums[env] < range.min) { + testsErrors[env] = sums[env]; + } + }); + if (Object.keys(testsErrors).length > 0) { + limitsErrors[test] = testsErrors; + } + }); + return limitsErrors; +}; + +export const validateLimitsSingleEnvironment = ( + { limits }, + env, + constraints +) => { + if (!limits || !env || !constraints) { + return false; + } + const envEnc = encodeId(env); + + // Compute sum of times for each environment. + let sum = 0; + Object.keys(limits).forEach(test => { + const memory = validateLimitsField( + safeGet(limits, [test, envEnc, 'memory']), + constraints.memory + ); + if (Number.isNaN(memory) || memory === false) { + return false; + } + + const time = safeGet(limits, [test, envEnc, 'time']); + const val = validateLimitsField(time, constraints.time); + if (Number.isNaN(val) || val === false) { + return false; + } + sum += val; + }); + + return sum >= constraints.totalTime.min && sum <= constraints.totalTime.max; +}; diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 98a9a692c..32d969be8 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -1,19 +1,7 @@ import yaml from 'js-yaml'; import { defaultMemoize } from 'reselect'; -import { - endpointDisguisedAsIdFactory, - encodeTestId, - encodeEnvironmentId -} from '../redux/modules/simpleLimits'; - -// safe getter to traverse compex object/array structures -const _safeGet = (obj, path) => { - path.forEach(step => { - obj = obj && (typeof step === 'function' ? obj.find(step) : obj[step]); - }); - return obj; -}; +import { safeGet, encodeNumId } from '../helpers/common'; /* * Tests and Score @@ -217,7 +205,7 @@ const getSimpleConfigCompilationVars = (testObj, config, environments) => { const compilation = {}; for (const environment of environments) { const pipelines = - _safeGet(config, [ + safeGet(config, [ c => c.name === environment.runtimeEnvironmentId, 'tests', t => t.name === testObj.name, @@ -289,7 +277,7 @@ export const getSimpleConfigInitValues = defaultMemoize( getSimpleConfigCompilationVars(testObj, config, environments); - res[encodeTestId(test.id)] = testObj; + res[encodeNumId(test.id)] = testObj; } return { @@ -362,7 +350,7 @@ const mergeOriginalVariables = (newVars, origVars) => { }; const mergeCompilationVariables = (origVars, testObj, envId) => { - const extraFiles = _safeGet(testObj, ['compilation', envId, 'extra-files']); + const extraFiles = safeGet(testObj, ['compilation', envId, 'extra-files']); if (!extraFiles) { return origVars; } @@ -415,12 +403,12 @@ export const transformConfigValues = ( let testsCfg = []; for (const t of tests) { const testName = t.id; - const test = formData.config[encodeTestId(testName)]; + const test = formData.config[encodeNumId(testName)]; const executionPipeline = test.useOutFile ? executionPipelineFiles : executionPipelineStdout; - const originalPipelines = _safeGet(originalConfig, [ + const originalPipelines = safeGet(originalConfig, [ config => config.name === envId, 'tests', t => t.name === testName, @@ -429,7 +417,7 @@ export const transformConfigValues = ( // Prepare variables for compilation pipeline ... const origCompilationVars = - _safeGet(originalPipelines, [ + safeGet(originalPipelines, [ p => p.name === compilationPipeline.id, 'variables' ]) || EMPTY_COMPILATION_PIPELINE_VARS; // if pipeline variables are not present, prepare an empty set @@ -442,7 +430,7 @@ export const transformConfigValues = ( // Prepare variables for execution pipeline ... const testVars = transformConfigTestExecutionVariables(test); - const origExecutionVars = _safeGet(originalPipelines, [ + const origExecutionVars = safeGet(originalPipelines, [ p => p.name === executionPipeline.id, 'variables' ]); @@ -472,95 +460,3 @@ export const transformConfigValues = ( return { config: envs }; }; - -/* - * Memory and Time limits - */ -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 - }; - }); - }); - - // 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'; - } - } - - return { - limits: res, - preciseTime - }; - } -); - -const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { - let res = { - memory - }; - res[timeField] = time; - return 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 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/hoc/withLinks.js b/src/helpers/withLinks.js similarity index 100% rename from src/hoc/withLinks.js rename to src/helpers/withLinks.js diff --git a/src/links/index.js b/src/links/index.js index 980bbe99d..a6486a423 100644 --- a/src/links/index.js +++ b/src/links/index.js @@ -32,6 +32,8 @@ export const linksFactory = lang => { `${EXERCISE_URI_FACTORY(id)}/edit-config`; const EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY = id => `${EXERCISE_URI_FACTORY(id)}/edit-simple-config`; + const EXERCISE_EDIT_LIMITS_URI_FACTORY = id => + `${EXERCISE_URI_FACTORY(id)}/edit-limits`; // reference solution const EXERCISE_REFERENCE_SOLUTION_URI_FACTORY = ( @@ -113,6 +115,7 @@ export const linksFactory = lang => { EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_CONFIG_URI_FACTORY, EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, + EXERCISE_EDIT_LIMITS_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 74c554b07..abf91c578 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -2,6 +2,8 @@ "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? Please 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? Please note, that individual environments have different performance characteristics.", + "app.EditLimitsForm.validation.NaN": "Given value is not a number.", + "app.EditLimitsForm.validation.outOfRange": "Given value is out of range. See limits constraints for details.", "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}).", @@ -43,8 +45,8 @@ "app.adminAssignmentsTable.noAssignments": "Nejsou dostupné žádné úlohy.", "app.adminAssignmentsTableRow.edit": "Upravit", "app.adminAssignmentsTableRow.loading": "Načítají se zadané úlohy ...", - "app.assignExerciseButton.isBroken": "Broken", - "app.assignExerciseButton.isLocked": "Locked", + "app.assignExerciseButton.isBroken": "Rozbitá", + "app.assignExerciseButton.isLocked": "Zamčená", "app.assignemntStatusIcon.evaluationFailed": "Žádné řesení nebylo správně vyhodnoceno.", "app.assignemntStatusIcon.failed": "Žádné správné řešení nebylo zatím odevzdáno.", "app.assignemntStatusIcon.inProgress": "Řešení úlohy je právě vyhodnocováno.", @@ -85,14 +87,13 @@ "app.assignments.deadline": "Termín odevzdání", "app.assignments.group": "Skupina", "app.assignments.name": "Název zadání", - "app.assignments.points": "Points", + "app.assignments.points": "Body", "app.assignments.secondDeadline": "Druhý termín odevzdání", "app.assignmentsTable.noAssignments": "Nenalezeny žádné zadané úlohy.", "app.assignmentsTableRow.loading": "Načítají se zadané úlohy ...", "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", @@ -167,10 +168,6 @@ "app.dashboard.studentOf": "Skupiny kde jste studentem", "app.dashboard.supervisorOf": "Skupiny kde jste cvičícím", "app.deleteButton.confirm": "Opravdu toto chcete smazat? Tato operace nemůže být vrácena.", - "app.deleteButton.delete": "Smazat", - "app.deleteButton.deleted": "Smazáno.", - "app.deleteButton.deleting": "Mazání ...", - "app.deleteButton.deletingFailed": "Mazání selhalo", "app.editAssignment.deleteAssignment": "Smazat zadání úlohy", "app.editAssignment.deleteAssignmentWarning": "Smazání zadané úlohy odstraní všechna studentská řešní. Pro případné obnovení těchto dat prosím kontaktujte správce ReCodExu.", "app.editAssignment.description": "Změnit nastavení zadání úlohy včetně jejích limitů", @@ -261,8 +258,8 @@ "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", + "app.editExerciseConfig.description": "Změnit nastavení testů úlohy", + "app.editExerciseConfig.title": "Změnit nastavení testů", "app.editExerciseConfigEnvironment.addConfigTab": "Přidat novou konfiguraci prostředí", "app.editExerciseConfigEnvironment.emptyConfigTabs": "Nyní zde není žádná konfigurace prostředí.", "app.editExerciseConfigEnvironment.newConfig": "Nová konfigurace", @@ -283,9 +280,8 @@ "app.editExerciseForm.difficulty": "Obtížnost", "app.editExerciseForm.easy": "Snadné", "app.editExerciseForm.failed": "Uložení se nezdařilo. Prosim, opakujte akci později.", - "app.editExerciseForm.gotoConfig": "Go to config", "app.editExerciseForm.hard": "Těžké", - "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", + "app.editExerciseForm.isLocked": "Úloha je zamčená (viditelná, ale není možné jí zadat v žádné skupině).", "app.editExerciseForm.isPublic": "Úloha je veřejná a může být použita cvičícími.", "app.editExerciseForm.medium": "Průměrné", "app.editExerciseForm.submit": "Upravit nastavení", @@ -302,8 +298,13 @@ "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.editExerciseLimits.description": "Změnit běhové limity testů úlohy", + "app.editExerciseLimits.missingSomething": "The limits can be set only when the exercise configuration is complete. The tests, runtime environments, and a hardware group must be properly set aprior to setting limits.", + "app.editExerciseLimits.missingSomethingTitle": "Exercise configuration is incomplete", + "app.editExerciseLimits.multiHwGroups": "The exercise uses complex configuration of multiple hardware groups. Editting the limits using this form may simplify this configuration. Proceed at your own risk.", + "app.editExerciseLimits.multiHwGroupsTitle": "Multiple hardware groups detected", + "app.editExerciseLimits.title": "Změnit limity testů", "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 ...", @@ -350,6 +351,9 @@ "app.editGroupForm.validation.localizedText.locale": "Please select the language.", "app.editGroupForm.validation.localizedText.text": "Please fill the description in this language.", "app.editGroupForm.validation.sameLocalizedTexts": "There are more language variants with the same locale. Please make sure locales are unique.", + "app.editHardwareGroupForm.failed": "Cannot change the hardware group of the exercise.", + "app.editHardwareGroupForm.hwGroupSelect": "Hardware Group:", + "app.editHardwareGroupForm.title": "Select Hardware Group", "app.editInstance.description": "Změnit nastavení instance", "app.editInstance.title": "Upravit instanci", "app.editInstanceForm.description": "Popis:", @@ -362,6 +366,19 @@ "app.editInstanceForm.title": "Upravit instanci", "app.editInstanceForm.validation.emptyName": "Please fill the name of the instance.", "app.editLimitsBox.title": "Edit limits", + "app.editLimitsField.tooltip.cloneAll": "Copy this value to all tests in all environments.", + "app.editLimitsField.tooltip.cloneHorizontal": "Copy this value horizontally to all environments of the test.", + "app.editLimitsField.tooltip.cloneVertical": "Copy this value vertically to all tests within the environment.", + "app.editLimitsForm.failed": "Nepodařilo se uložit limity. Prosím, opakujte akci později.", + "app.editLimitsForm.preciseTime": "Precise Time Measurement", + "app.editLimitsForm.preciseTimeTooltip": "If precise time measurement is selected, ReCodEx will measure the consumed CPU time of tested solutions. 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 (including I/O), but it is more susceptible to errors of measurement.", + "app.editLimitsForm.reset": "Obnovit původní", + "app.editLimitsForm.submit": "Uložit limity", + "app.editLimitsForm.submitting": "Ukládám limity ...", + "app.editLimitsForm.success": "Limity uloženy.", + "app.editLimitsForm.validating": "Validuji ...", + "app.editLimitsForm.validation.timeSum": "Součet časových limitů ({sum}) překračuje povolené maximum ({max}).", + "app.editLimitsForm.validation.totalTime": "The time limits total is out of range. See limits constraints for details.", "app.editLocalizedTextForm.addLanguage": "Add language variant", "app.editLocalizedTextForm.localized.noLanguage": "There is currently no text in any language.", "app.editLocalizedTextForm.localized.reallyRemoveQuestion": "Do you really want to delete this localization?", @@ -393,15 +410,15 @@ "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.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 solutions. 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 (including 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 ...", - "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.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.editSisTerm.advertiseUntil": "Advertise this term to students until:", "app.editSisTerm.beginning": "Beginning of the term:", "app.editSisTerm.close": "Close", @@ -503,15 +520,12 @@ "app.exercise.createPipeline": "Add exercise pipeline", "app.exercise.createReferenceSoution": "Vytvořit referenční řešení", "app.exercise.createdAt": "Vytvořeno:", - "app.exercise.deleteButton": "Smazat", - "app.exercise.deleteConfirm": "Opravdu chcete odstranit tuto úlohu? Tato akce je nevratná.", "app.exercise.description": "Přehled úlohy", "app.exercise.detailTitle": "Popis úlohy", "app.exercise.difficulty": "Obtížnost", - "app.exercise.editButton": "Upravit", - "app.exercise.editConfig": "Edit exercise config", - "app.exercise.editConfigButton": "Edit config", - "app.exercise.editSettings": "Upravit nastavení úlohy", + "app.exercise.editConfig": "Konfigurace testů", + "app.exercise.editLimits": "Limity testů", + "app.exercise.editSettings": "Nastavení úlohy", "app.exercise.exercisePipelines": "Exercise Pipelines", "app.exercise.forked": "Duplikováno z:", "app.exercise.groups": "Groups:", @@ -521,7 +535,6 @@ "app.exercise.isPublic": "Is public:", "app.exercise.noReferenceSolutions": "Nyní zde nejsou žádná referenční řešení pro tuto úlohu.", "app.exercise.publicGroup": "Public", - "app.exercise.referenceSolution.deleteButton": "Delete", "app.exercise.referenceSolution.deleteConfirm": "Are you sure you want to delete the reference solution? This cannot be undone.", "app.exercise.referenceSolutionDetail": "Detail referenčního řešení", "app.exercise.referenceSolutionEvaluationDescription": "Evaluation", @@ -539,8 +552,9 @@ "app.exercises.difficultyIcon.hard": "Obtížné", "app.exercises.difficultyIcon.medium": "Průměrné", "app.exercises.failedDetail": "Načtení detailu úlohy se nezdařilo. Prosíme zkontrolujte své připojení k internetu a zkuste dotaz opakovat později.", - "app.exercises.listEdit": "Upravit", - "app.exercises.listEditConfig": "Edit config", + "app.exercises.listEdit": "Nastavení", + "app.exercises.listEditConfig": "Konfigurace", + "app.exercises.listEditLimits": "Limits", "app.exercises.listTitle": "Úlohy", "app.exercises.loadingDetail": "Načítání detailu úlohy", "app.exercises.referenceSolutionDescription": "Popis", @@ -550,9 +564,9 @@ "app.exercisesList.created": "Vytvořeno", "app.exercisesList.difficulty": "Obtížnost", "app.exercisesList.empty": "Nenalezeny žádné úlohy k zobrazení.", - "app.exercisesList.groups": "Groups", + "app.exercisesList.groups": "Skupiny", "app.exercisesList.name": "Jméno", - "app.exercisesListItem.group.public": "Veřejné", + "app.exercisesListItem.noGroups": "žádné skupiny", "app.exercisesName.loading": "Loading ...", "app.exercisesSimpleList.difficulty": "Obtížnost", "app.exercisesSimpleList.empty": "V tomto seznamu nejsou žádné úlohy.", @@ -730,6 +744,16 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Odebrat cvičícího", "app.groupsName.loading": "Načítání ...", + "app.hardwareGroupMetadata.cpuTimeOverlay": "Precise (CPU) time limit constraints", + "app.hardwareGroupMetadata.description": "Internal Description:", + "app.hardwareGroupMetadata.id": "Internal Identifier:", + "app.hardwareGroupMetadata.memoryConstraints": "Memory Constraints:", + "app.hardwareGroupMetadata.memoryOverlay": "Memory limit constraints", + "app.hardwareGroupMetadata.timeOverlay": "Both precise (CPU) and wall time limit constraints", + "app.hardwareGroupMetadata.timePerExerciseConstraints": "Total Time Per Exercise Constraints:", + "app.hardwareGroupMetadata.timePerTestConstraints": "Time Per Test Constraints:", + "app.hardwareGroupMetadata.title": "Hardware Group Metadata", + "app.hardwareGroupMetadata.wallTimeOverlay": "Wall time limit constraints", "app.header.toggleSidebar": "Zobrazit/skrýt boční panel", "app.header.toggleSidebarSize": "Zvětšit/zmenšit boční panel", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -997,7 +1021,6 @@ "app.sisCreateGroupForm.success": "The group was created.", "app.sisCreateGroupForm.title": "Create ReCodEx group from SIS", "app.sisIntegration.courseId": "Course ID", - "app.sisIntegration.deleteButton": "Delete", "app.sisIntegration.deleteConfirm": "Are you sure you want to delete the SIS term?", "app.sisIntegration.description": "Integration with university SIS system", "app.sisIntegration.editButton": "Edit", @@ -1148,6 +1171,14 @@ "app.usersStats.description": "Body získané ve skupině {name}.", "app.usersname.notVerified.description": "Tento uživatel si neověřil svou emailovou adresu přes aktivační odkaz, který mu byl na tuto adresu zaslán.", "diff": "Sudí binárních dat", + "generic.delete": "Smazat", + "generic.deleteFailed": "Smazání selhalo", + "generic.deleted": "Smazáno", + "generic.deleting": "Odstraňování ...", + "generic.reset": "Reset", + "generic.save": "Save", + "generic.saved": "Saved", + "generic.saving": "Saving ...", "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ů", diff --git a/src/locales/en.json b/src/locales/en.json index 7387e4096..2127990bd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,6 +2,8 @@ "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? Please 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? Please note, that individual environments have different performance characteristics.", + "app.EditLimitsForm.validation.NaN": "Given value is not a number.", + "app.EditLimitsForm.validation.outOfRange": "Given value is out of range. See limits constraints for details.", "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}).", @@ -92,7 +94,6 @@ "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", @@ -167,10 +168,6 @@ "app.dashboard.studentOf": "Groups you are student of", "app.dashboard.supervisorOf": "Groups you supervise", "app.deleteButton.confirm": "Are you sure you want to delete the resource? This cannot be undone.", - "app.deleteButton.delete": "Delete", - "app.deleteButton.deleted": "Deleted.", - "app.deleteButton.deleting": "Deleting ...", - "app.deleteButton.deletingFailed": "Deleting failed", "app.editAssignment.deleteAssignment": "Delete the assignment", "app.editAssignment.deleteAssignmentWarning": "Deleting an assignment will remove all the students submissions and you will have to contact the administrator of ReCodEx if you wanted to restore the assignment in the future.", "app.editAssignment.description": "Change assignment settings and limits", @@ -261,8 +258,8 @@ "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", + "app.editExerciseConfig.description": "Change exercise tests configuration", + "app.editExerciseConfig.title": "Edit tests configuration", "app.editExerciseConfigEnvironment.addConfigTab": "Add new runtime configuration", "app.editExerciseConfigEnvironment.emptyConfigTabs": "There is currently no runtime configuration.", "app.editExerciseConfigEnvironment.newConfig": "New configuration", @@ -283,7 +280,6 @@ "app.editExerciseForm.difficulty": "Difficulty", "app.editExerciseForm.easy": "Easy", "app.editExerciseForm.failed": "Saving failed. Please try again later.", - "app.editExerciseForm.gotoConfig": "Go to config", "app.editExerciseForm.hard": "Hard", "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", "app.editExerciseForm.isPublic": "Exercise is public and can be assigned to students by their supervisors.", @@ -302,8 +298,13 @@ "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.editExerciseLimits.description": "Change exercise tests execution limits", + "app.editExerciseLimits.missingSomething": "The limits can be set only when the exercise configuration is complete. The tests, runtime environments, and a hardware group must be properly set aprior to setting limits.", + "app.editExerciseLimits.missingSomethingTitle": "Exercise configuration is incomplete", + "app.editExerciseLimits.multiHwGroups": "The exercise uses complex configuration of multiple hardware groups. Editting the limits using this form may simplify this configuration. Proceed at your own risk.", + "app.editExerciseLimits.multiHwGroupsTitle": "Multiple hardware groups detected", + "app.editExerciseLimits.title": "Edit tests limits", "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 ...", @@ -350,6 +351,9 @@ "app.editGroupForm.validation.localizedText.locale": "Please select the language.", "app.editGroupForm.validation.localizedText.text": "Please fill the description in this language.", "app.editGroupForm.validation.sameLocalizedTexts": "There are more language variants with the same locale. Please make sure locales are unique.", + "app.editHardwareGroupForm.failed": "Cannot change the hardware group of the exercise.", + "app.editHardwareGroupForm.hwGroupSelect": "Hardware Group:", + "app.editHardwareGroupForm.title": "Select Hardware Group", "app.editInstance.description": "Change instance settings", "app.editInstance.title": "Edit instance", "app.editInstanceForm.description": "Description:", @@ -362,6 +366,19 @@ "app.editInstanceForm.title": "Edit instance", "app.editInstanceForm.validation.emptyName": "Please fill the name of the instance.", "app.editLimitsBox.title": "Edit limits", + "app.editLimitsField.tooltip.cloneAll": "Copy this value to all tests in all environments.", + "app.editLimitsField.tooltip.cloneHorizontal": "Copy this value horizontally to all environments of the test.", + "app.editLimitsField.tooltip.cloneVertical": "Copy this value vertically to all tests within the environment.", + "app.editLimitsForm.failed": "Cannot save the exercise limits. Please try again later.", + "app.editLimitsForm.preciseTime": "Precise Time Measurement", + "app.editLimitsForm.preciseTimeTooltip": "If precise time measurement is selected, ReCodEx will measure the consumed CPU time of tested solutions. 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 (including I/O), but it is more susceptible to errors of measurement.", + "app.editLimitsForm.reset": "Reset", + "app.editLimitsForm.submit": "Save Limits", + "app.editLimitsForm.submitting": "Saving Limits ...", + "app.editLimitsForm.success": "Limits Saved", + "app.editLimitsForm.validating": "Validating ...", + "app.editLimitsForm.validation.timeSum": "The sum of time limits ({sum}) exceeds allowed maximum ({max}).", + "app.editLimitsForm.validation.totalTime": "The time limits total is out of range. See limits constraints for details.", "app.editLocalizedTextForm.addLanguage": "Add language variant", "app.editLocalizedTextForm.localized.noLanguage": "There is currently no text in any language.", "app.editLocalizedTextForm.localized.reallyRemoveQuestion": "Do you really want to delete this localization?", @@ -503,15 +520,12 @@ "app.exercise.createPipeline": "Add exercise pipeline", "app.exercise.createReferenceSoution": "Create reference solution", "app.exercise.createdAt": "Created at:", - "app.exercise.deleteButton": "Delete", - "app.exercise.deleteConfirm": "Are you sure you want to delete the exercise? This cannot be undone.", "app.exercise.description": "Exercise overview", "app.exercise.detailTitle": "Exercise description", "app.exercise.difficulty": "Difficulty", - "app.exercise.editButton": "Edit", - "app.exercise.editConfig": "Edit exercise config", - "app.exercise.editConfigButton": "Edit config", - "app.exercise.editSettings": "Edit exercise settings", + "app.exercise.editConfig": "Tests Configuration", + "app.exercise.editLimits": "Tests Limits", + "app.exercise.editSettings": "Exercise Settings", "app.exercise.exercisePipelines": "Exercise Pipelines", "app.exercise.forked": "Forked from:", "app.exercise.groups": "Groups:", @@ -521,7 +535,6 @@ "app.exercise.isPublic": "Is public:", "app.exercise.noReferenceSolutions": "There are no reference solutions for this exercise yet.", "app.exercise.publicGroup": "Public", - "app.exercise.referenceSolution.deleteButton": "Delete", "app.exercise.referenceSolution.deleteConfirm": "Are you sure you want to delete the reference solution? This cannot be undone.", "app.exercise.referenceSolutionDetail": "Reference solution detail", "app.exercise.referenceSolutionEvaluationDescription": "Evaluation", @@ -539,8 +552,9 @@ "app.exercises.difficultyIcon.hard": "Hard", "app.exercises.difficultyIcon.medium": "Medium", "app.exercises.failedDetail": "Loading the details of the exercise failed. Please make sure you are connected to the Internet and try again later.", - "app.exercises.listEdit": "Edit", - "app.exercises.listEditConfig": "Edit config", + "app.exercises.listEdit": "Settings", + "app.exercises.listEditConfig": "Configuration", + "app.exercises.listEditLimits": "Limits", "app.exercises.listTitle": "Exercises", "app.exercises.loadingDetail": "Loading exercise's detail", "app.exercises.referenceSolutionDescription": "Description", @@ -552,7 +566,7 @@ "app.exercisesList.empty": "There are no exercises in this list.", "app.exercisesList.groups": "Groups", "app.exercisesList.name": "Name", - "app.exercisesListItem.group.public": "Public", + "app.exercisesListItem.noGroups": "no groups", "app.exercisesName.loading": "Loading ...", "app.exercisesSimpleList.difficulty": "Difficulty", "app.exercisesSimpleList.empty": "There are no exercises in this list.", @@ -730,6 +744,16 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Remove supervisor", "app.groupsName.loading": "Loading ...", + "app.hardwareGroupMetadata.cpuTimeOverlay": "Precise (CPU) time limit constraints", + "app.hardwareGroupMetadata.description": "Internal Description:", + "app.hardwareGroupMetadata.id": "Internal Identifier:", + "app.hardwareGroupMetadata.memoryConstraints": "Memory Constraints:", + "app.hardwareGroupMetadata.memoryOverlay": "Memory limit constraints", + "app.hardwareGroupMetadata.timeOverlay": "Both precise (CPU) and wall time limit constraints", + "app.hardwareGroupMetadata.timePerExerciseConstraints": "Total Time Per Exercise Constraints:", + "app.hardwareGroupMetadata.timePerTestConstraints": "Time Per Test Constraints:", + "app.hardwareGroupMetadata.title": "Hardware Group Metadata", + "app.hardwareGroupMetadata.wallTimeOverlay": "Wall time limit constraints", "app.header.toggleSidebar": "Show/hide sidebar", "app.header.toggleSidebarSize": "Expand/minimize sidebar", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -997,7 +1021,6 @@ "app.sisCreateGroupForm.success": "The group was created.", "app.sisCreateGroupForm.title": "Create ReCodEx group from SIS", "app.sisIntegration.courseId": "Course ID", - "app.sisIntegration.deleteButton": "Delete", "app.sisIntegration.deleteConfirm": "Are you sure you want to delete the SIS term?", "app.sisIntegration.description": "Integration with university SIS system", "app.sisIntegration.editButton": "Edit", @@ -1148,6 +1171,14 @@ "app.usersStats.description": "Points gained from {name}.", "app.usersname.notVerified.description": "This user has not verified his/her email address via an activation link he has received to his email address.", "diff": "Binary-safe judge", + "generic.delete": "Delete", + "generic.deleteFailed": "Delete Failed", + "generic.deleted": "Deleted", + "generic.deleting": "Deleting ...", + "generic.reset": "Reset", + "generic.save": "Save", + "generic.saved": "Saved", + "generic.saving": "Saving ...", "recodex-judge-float": "Float-numbers judge", "recodex-judge-float-newline": "Float-numbers judge (ignoring ends of lines)", "recodex-judge-normal": "Token judge", diff --git a/src/pages/Assignment/Assignment.js b/src/pages/Assignment/Assignment.js index a5872b356..ab765ebcc 100644 --- a/src/pages/Assignment/Assignment.js +++ b/src/pages/Assignment/Assignment.js @@ -48,7 +48,7 @@ import SubmitSolutionContainer from '../../containers/SubmitSolutionContainer'; import SubmissionsTable from '../../components/Assignments/SubmissionsTable'; import AssignmentSync from '../../components/Assignments/Assignment/AssignmentSync'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Assignment extends Component { static loadAsync = ({ assignmentId }, dispatch, userId) => diff --git a/src/pages/ChangePassword/ChangePassword.js b/src/pages/ChangePassword/ChangePassword.js index bcaa869ac..d5164d7bc 100644 --- a/src/pages/ChangePassword/ChangePassword.js +++ b/src/pages/ChangePassword/ChangePassword.js @@ -18,7 +18,7 @@ import { hasChangingSucceeded as hasSucceeded } from '../../redux/selectors/auth'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; /** * Component for changing old password for a new one for a user with a specific diff --git a/src/pages/Dashboard/Dashboard.js b/src/pages/Dashboard/Dashboard.js index 0befdcb60..0c747dd6c 100644 --- a/src/pages/Dashboard/Dashboard.js +++ b/src/pages/Dashboard/Dashboard.js @@ -8,6 +8,7 @@ import Button from '../../components/widgets/FlatButton'; import { Link } from 'react-router'; import { LinkContainer } from 'react-router-bootstrap'; +import { EMPTY_OBJ } from '../../helpers/common'; import Page from '../../components/layout/Page'; import Box from '../../components/widgets/Box'; import { LoadingInfoBox } from '../../components/widgets/InfoBox'; @@ -46,9 +47,7 @@ import SisIntegrationContainer from '../../containers/SisIntegrationContainer'; import SisSupervisorGroupsContainer from '../../containers/SisSupervisorGroupsContainer'; import { getLocalizedName } from '../../helpers/getLocalizedData'; -import withLinks from '../../hoc/withLinks'; - -const EMPTY_OBJ = {}; +import withLinks from '../../helpers/withLinks'; class Dashboard extends Component { componentDidMount = () => this.props.loadAsync(this.props.userId); diff --git a/src/pages/EditAssignment/EditAssignment.js b/src/pages/EditAssignment/EditAssignment.js index 0cc48f676..658d463b7 100644 --- a/src/pages/EditAssignment/EditAssignment.js +++ b/src/pages/EditAssignment/EditAssignment.js @@ -34,7 +34,7 @@ import { ResubmitAllSolutionsContainer } from '../../containers/ResubmitSolution import AssignmentSync from '../../components/Assignments/Assignment/AssignmentSync'; import { getLocalizedTextsLocales } from '../../helpers/getLocalizedData'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; class EditAssignment extends Component { diff --git a/src/pages/EditExercise/EditExercise.js b/src/pages/EditExercise/EditExercise.js index e181bebac..d76e1baf7 100644 --- a/src/pages/EditExercise/EditExercise.js +++ b/src/pages/EditExercise/EditExercise.js @@ -14,6 +14,7 @@ import EditExerciseForm from '../../components/forms/EditExerciseForm'; import AttachmentFilesTableContainer from '../../containers/AttachmentFilesTableContainer'; import DeleteExerciseButtonContainer from '../../containers/DeleteExerciseButtonContainer'; import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; +import ExerciseButtons from '../../components/Exercises/ExerciseButtons'; import { fetchExerciseIfNeeded, @@ -23,7 +24,7 @@ import { getExercise } from '../../redux/selectors/exercises'; import { isSubmitting } from '../../redux/selectors/submission'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import { getLocalizedName, getLocalizedTextsLocales @@ -111,6 +112,11 @@ class EditExercise extends Component {
} + + + + + Promise.all([ - dispatch(fetchExerciseIfNeeded(exerciseId)).then(({ value: exercise }) => - Promise.all( - exercise.runtimeEnvironments.map(environment => - dispatch( - fetchExerciseEnvironmentSimpleLimitsIfNeeded( - exerciseId, - environment.id - ) - ) - ) - ) - ), + dispatch(fetchExerciseIfNeeded(exerciseId)), dispatch(fetchExerciseConfigIfNeeded(exerciseId)), dispatch(fetchRuntimeEnvironments()), dispatch(fetchExerciseEnvironmentConfigIfNeeded(exerciseId)), @@ -90,9 +74,7 @@ class EditExerciseConfig extends Component { exerciseConfig, exerciseEnvironmentConfig, exerciseScoreConfig, - // editEnvironmentSimpleLimits, pipelines, - // limits, editScoreConfig, superadmin, intl: { locale } @@ -203,16 +185,6 @@ class EditExerciseConfig extends Component { exercise={exercise} pipelines={pipelines} />} - {/* Limit editation was completely redefined in simple form. - */} } @@ -236,10 +208,8 @@ EditExerciseConfig.propTypes = { exerciseConfig: PropTypes.object, exerciseEnvironmentConfig: PropTypes.object, exerciseScoreConfig: PropTypes.object, - editEnvironmentSimpleLimits: PropTypes.func.isRequired, pipelines: ImmutablePropTypes.map, links: PropTypes.object.isRequired, - limits: PropTypes.func.isRequired, editScoreConfig: PropTypes.func.isRequired, superadmin: PropTypes.bool.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired @@ -260,8 +230,6 @@ export default injectIntl( exerciseId )(state), pipelines: pipelinesSelector(state), - limits: runtimeEnvironmentId => - simpleLimitsSelector(exerciseId, runtimeEnvironmentId)(state), superadmin: isLoggedAsSuperAdmin(state) }; }, @@ -269,10 +237,6 @@ export default injectIntl( loadAsync: () => EditExerciseConfig.loadAsync({ exerciseId }, dispatch), editEnvironmentConfigs: data => dispatch(setExerciseEnvironmentConfig(exerciseId, data)), - editEnvironmentSimpleLimits: runtimeEnvironmentId => data => - dispatch( - editEnvironmentSimpleLimits(exerciseId, runtimeEnvironmentId, data) - ), setConfig: data => dispatch(setExerciseConfig(exerciseId, data)), editScoreConfig: data => dispatch(setScoreConfig(exerciseId, data)) }) diff --git a/src/pages/EditExerciseLimits/EditExerciseLimits.js b/src/pages/EditExerciseLimits/EditExerciseLimits.js new file mode 100644 index 000000000..b3160fe13 --- /dev/null +++ b/src/pages/EditExerciseLimits/EditExerciseLimits.js @@ -0,0 +1,446 @@ +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 { defaultMemoize } from 'reselect'; +import Icon from 'react-fontawesome'; +import { formValueSelector } from 'redux-form'; + +import Page from '../../components/layout/Page'; +import HardwareGroupMetadata from '../../components/Exercises/HardwareGroupMetadata'; +import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; +import EditHardwareGroupForm from '../../components/forms/EditHardwareGroupForm'; +import EditLimitsForm from '../../components/forms/EditLimitsForm/EditLimitsForm'; +import ExerciseButtons from '../../components/Exercises/ExerciseButtons'; +import ResourceRenderer from '../../components/helpers/ResourceRenderer'; + +import { + fetchExercise, + fetchExerciseIfNeeded, + setExerciseHardwareGroups +} from '../../redux/modules/exercises'; +import { + fetchExerciseEnvironmentLimits, + fetchExerciseEnvironmentLimitsIfNeeded, + editEnvironmentLimits, + cloneHorizontally, + cloneVertically, + cloneAll +} from '../../redux/modules/limits'; +import { fetchHardwareGroups } from '../../redux/modules/hwGroups'; +import { getExercise } from '../../redux/selectors/exercises'; +import { loggedInUserIdSelector } from '../../redux/selectors/auth'; +import { limitsSelector } from '../../redux/selectors/limits'; +import { hardwareGroupsSelector } from '../../redux/selectors/hwGroups'; +import { isLoggedAsSuperAdmin } from '../../redux/selectors/users'; + +import withLinks from '../../helpers/withLinks'; +import { getLocalizedName } from '../../helpers/getLocalizedData'; +import { fetchExerciseTestsIfNeeded } from '../../redux/modules/exerciseTests'; +import { exerciseTestsSelector } from '../../redux/selectors/exerciseTests'; + +import { + getLimitsInitValues, + transformLimitsValues, + getLimitsConstraints, + validateLimitsSingleEnvironment +} from '../../helpers/exerciseLimits'; + +class EditExerciseLimits extends Component { + componentWillMount = () => this.props.loadAsync(); + + componentWillReceiveProps = nextProps => { + if (this.props.params.exerciseId !== nextProps.params.exerciseId) { + nextProps.loadAsync(); + } + }; + + static loadAsync = ({ exerciseId }, dispatch) => + Promise.all([ + dispatch(fetchHardwareGroups()), + dispatch(fetchExerciseIfNeeded(exerciseId)).then( + ({ value: exercise }) => + exercise.hardwareGroups && exercise.hardwareGroups.length === 1 + ? Promise.all( + exercise.runtimeEnvironments.map(environment => + dispatch( + fetchExerciseEnvironmentLimitsIfNeeded( + exerciseId, + environment.id, + exercise.hardwareGroups[0].id + ) + ) + ) + ) + : Promise.resolve() + ), + dispatch(fetchExerciseTestsIfNeeded(exerciseId)) + ]); + + transformAndSendHardwareGroups = defaultMemoize( + (exerciseId, hwGroupId, limits, tests, exerciseRuntimeEnvironments) => { + const { + setExerciseHardwareGroups, + editEnvironmentLimits, + fetchExerciseEnvironmentLimit + } = this.props; + const limitsData = + hwGroupId && + getLimitsInitValues( + limits, + tests, + exerciseRuntimeEnvironments, + exerciseId, + hwGroupId + ); + + return formData => + setExerciseHardwareGroups( + formData.hardwareGroup ? [formData.hardwareGroup] : [] + ).then( + ({ value: exercise }) => + limitsData + ? Promise.all( + exercise.hardwareGroups.map(({ id: hwgId }, idx) => { + const constraints = getLimitsConstraints( + exercise.hardwareGroups, + limitsData.preciseTime + ); + return Promise.all( + transformLimitsValues( + limitsData, + tests, + exerciseRuntimeEnvironments + ).map( + ({ id: envId, data }) => + validateLimitsSingleEnvironment( + limitsData, + envId, + constraints + ) + ? editEnvironmentLimits(hwgId, envId, data) + : idx === 0 + ? fetchExerciseEnvironmentLimit(envId, hwgId) + : Promise.resolve() + ) + ); + }) + ) + : Promise.resolve() + ); + } + ); + + transformAndSendLimitsValues = defaultMemoize( + (tests, exerciseRuntimeEnvironments) => { + const { exercise, editEnvironmentLimits, reloadExercise } = this.props; + return formData => + Promise.all( + transformLimitsValues( + formData, + tests, + exerciseRuntimeEnvironments + ).map(({ id, data }) => + editEnvironmentLimits( + exercise.getIn(['data', 'hardwareGroups', 0, 'id']), + id, + data + ) + ) + ).then(reloadExercise); + } + ); + + render() { + const { + links: { EXERCISE_URI_FACTORY }, + params: { exerciseId }, + exercise, + exerciseTests, + limits, + hardwareGroups, + preciseTime, + targetHardwareGroup, + isSuperAdmin, + cloneHorizontally, + cloneVertically, + cloneAll, + 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, tests) => +
+ {exercise.isBroken && + + +
+

+    + +

+ {exercise.validationError} +
+ +
} + + + + + + + {Boolean( + exercise.hardwareGroups && exercise.hardwareGroups.length > 1 + ) && + + +
+

+ {' '} + +

+

+ +

+
+ +
} + + + {hwgs => + + + + + + {exercise.hardwareGroups.map(h => + + )} + {Boolean( + targetHardwareGroup && + (exercise.hardwareGroups.length !== 1 || + targetHardwareGroup !== exercise.hardwareGroups[0].id) + ) && +
+
+    +
+ h.id === targetHardwareGroup + )} + isSuperAdmin={isSuperAdmin} + /> +
} + +
} +
+ + + + {tests.length > 0 && + exercise.runtimeEnvironments.length > 0 && + exercise.hardwareGroups.length > 0 + ? + :
+

+ {' '} + +

+ +
} + +
+
} +
+ ); + } +} + +EditExerciseLimits.propTypes = { + exercise: ImmutablePropTypes.map, + loadAsync: PropTypes.func.isRequired, + params: PropTypes.shape({ + exerciseId: PropTypes.string.isRequired + }).isRequired, + editEnvironmentLimits: PropTypes.func.isRequired, + setExerciseHardwareGroups: PropTypes.func.isRequired, + fetchExerciseEnvironmentLimit: PropTypes.func.isRequired, + exerciseTests: PropTypes.object, + limits: PropTypes.object.isRequired, + hardwareGroups: ImmutablePropTypes.map, + preciseTime: PropTypes.bool, + targetHardwareGroup: PropTypes.string, + isSuperAdmin: PropTypes.bool.isRequired, + cloneHorizontally: PropTypes.func.isRequired, + cloneVertically: PropTypes.func.isRequired, + cloneAll: PropTypes.func.isRequired, + reloadExercise: PropTypes.func.isRequired, + links: PropTypes.object.isRequired, + 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)) +); + +const editLimitsFormSelector = formValueSelector('editLimits'); +const editHardwareGroupFormSelector = formValueSelector('editHardwareGroup'); + +export default withLinks( + connect( + (state, { params: { exerciseId } }) => { + return { + exercise: getExercise(exerciseId)(state), + userId: loggedInUserIdSelector(state), + limits: limitsSelector(state), + exerciseTests: exerciseTestsSelector(exerciseId)(state), + hardwareGroups: hardwareGroupsSelector(state), + preciseTime: editLimitsFormSelector(state, 'preciseTime'), + targetHardwareGroup: editHardwareGroupFormSelector( + state, + 'hardwareGroup' + ), + isSuperAdmin: isLoggedAsSuperAdmin(state) + }; + }, + (dispatch, { params: { exerciseId } }) => ({ + loadAsync: () => EditExerciseLimits.loadAsync({ exerciseId }, dispatch), + setExerciseHardwareGroups: hwGroups => + dispatch(setExerciseHardwareGroups(exerciseId, hwGroups)), + editEnvironmentLimits: (hwGroupId, runtimeEnvironmentId, data) => + dispatch( + editEnvironmentLimits( + exerciseId, + runtimeEnvironmentId, + hwGroupId, + data + ) + ), + cloneVertically: cloneVerticallyWrapper(dispatch), + cloneHorizontally: cloneHorizontallyWrapper(dispatch), + cloneAll: cloneAllWrapper(dispatch), + fetchExerciseEnvironmentLimit: (envId, hwgId) => + dispatch(fetchExerciseEnvironmentLimits(exerciseId, envId, hwgId)), + reloadExercise: () => dispatch(fetchExercise(exerciseId)) + }) + )(injectIntl(EditExerciseLimits)) +); diff --git a/src/pages/EditExerciseLimits/index.js b/src/pages/EditExerciseLimits/index.js new file mode 100644 index 000000000..50708e3b4 --- /dev/null +++ b/src/pages/EditExerciseLimits/index.js @@ -0,0 +1 @@ +export default from './EditExerciseLimits'; diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index 61b5b7ef4..8e839ab7b 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -11,24 +11,16 @@ 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 ExerciseButtons from '../../components/Exercises/ExerciseButtons'; import { fetchExercise, fetchExerciseIfNeeded } from '../../redux/modules/exercises'; -import { - fetchExerciseEnvironmentSimpleLimitsIfNeeded, - editEnvironmentSimpleLimits, - cloneHorizontally, - cloneVertically, - cloneAll, - fetchExerciseEnvironmentSimpleLimits -} from '../../redux/modules/simpleLimits'; import { fetchExerciseConfig, fetchExerciseConfigIfNeeded, @@ -39,9 +31,8 @@ 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 withLinks from '../../helpers/withLinks'; import { getLocalizedName } from '../../helpers/getLocalizedData'; import { exerciseEnvironmentConfigSelector } from '../../redux/selectors/exerciseEnvironmentConfigs'; import { @@ -68,9 +59,7 @@ import { getTestsInitValues, transformTestsValues, getSimpleConfigInitValues, - transformConfigValues, - getLimitsInitValues, - transformLimitsValues + transformConfigValues } from '../../helpers/exerciseSimpleForm'; class EditExerciseSimpleConfig extends Component { @@ -84,18 +73,7 @@ class EditExerciseSimpleConfig extends Component { static loadAsync = ({ exerciseId }, dispatch) => Promise.all([ - dispatch(fetchExerciseIfNeeded(exerciseId)).then(({ value: exercise }) => - Promise.all( - exercise.runtimeEnvironments.map(environment => - dispatch( - fetchExerciseEnvironmentSimpleLimitsIfNeeded( - exerciseId, - environment.id - ) - ) - ) - ) - ), + dispatch(fetchExerciseIfNeeded(exerciseId)), dispatch(fetchExerciseConfigIfNeeded(exerciseId)), dispatch(fetchExerciseEnvironmentConfigIfNeeded(exerciseId)), dispatch(fetchScoreConfigIfNeeded(exerciseId)), @@ -105,12 +83,12 @@ class EditExerciseSimpleConfig extends Component { ]); transformAndSendTestsValues = data => { - const { editTests, editScoreConfig, reloadConfigAndLimits } = this.props; + const { editTests, editScoreConfig, reloadConfig } = this.props; const { tests, scoreConfig } = transformTestsValues(data); return Promise.all([ editTests({ tests }), editScoreConfig({ scoreConfig }) - ]).then(reloadConfigAndLimits); + ]).then(reloadConfig); }; transformAndSendConfigValuesCreator = defaultMemoize( @@ -125,11 +103,7 @@ class EditExerciseSimpleConfig extends Component { transformAndSendEnvValues = defaultMemoize( (pipelines, environments, tests, config) => { - const { - editEnvironmentConfigs, - reloadConfigAndLimits, - setConfig - } = this.props; + const { editEnvironmentConfigs, reloadConfig, setConfig } = this.props; return data => { const newEnvironments = transformEnvValues(data, environments); @@ -144,25 +118,11 @@ class EditExerciseSimpleConfig extends Component { environmentConfigs: newEnvironments }) .then(() => setConfig(configData)) - .then(reloadConfigAndLimits); + .then(reloadConfig); }; } ); - transformAndSendLimitsValues = defaultMemoize( - (tests, exerciseRuntimeEnvironments) => { - const { editEnvironmentSimpleLimits, reloadExercise } = this.props; - return formData => - Promise.all( - transformLimitsValues( - formData, - tests, - exerciseRuntimeEnvironments - ).map(({ id, data }) => editEnvironmentSimpleLimits(id, data)) - ).then(reloadExercise); - } - ); - render() { const { links: { EXERCISE_URI_FACTORY }, @@ -173,11 +133,7 @@ class EditExerciseSimpleConfig extends Component { exerciseEnvironmentConfig, exerciseScoreConfig, exerciseTests, - limits, pipelines, - cloneHorizontally, - cloneVertically, - cloneAll, intl: { locale } } = this.props; @@ -188,7 +144,7 @@ class EditExerciseSimpleConfig extends Component { description={ } breadcrumbs={[ @@ -212,7 +168,7 @@ class EditExerciseSimpleConfig extends Component { text: ( ), iconName: 'pencil' @@ -244,6 +200,11 @@ class EditExerciseSimpleConfig extends Component {
} + + + + + } - - - - - {envConfig => - tests.length > 0 && exercise.runtimeEnvironments.length > 0 - ? - :
-

- {' '} - -

- -
} -
- -
} ); @@ -406,8 +326,6 @@ EditExerciseSimpleConfig.propTypes = { 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, @@ -415,97 +333,45 @@ EditExerciseSimpleConfig.propTypes = { 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, reloadExercise: PropTypes.func.isRequired, - reloadConfigAndLimits: PropTypes.func.isRequired, + reloadConfig: PropTypes.func.isRequired, 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( - (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: cloneVerticallyWrapper(dispatch), - cloneHorizontally: cloneHorizontallyWrapper(dispatch), - cloneAll: cloneAllWrapper(dispatch), - reloadExercise: () => dispatch(fetchExercise(exerciseId)), - reloadConfigAndLimits: () => - dispatch(fetchExercise(exerciseId)).then(({ value: exercise }) => - Promise.all([ - dispatch(fetchExerciseConfig(exerciseId)), - dispatch(fetchExerciseEnvironmentConfig(exerciseId)), - ...exercise.runtimeEnvironments.map(environment => - dispatch( - fetchExerciseEnvironmentSimpleLimits( - exerciseId, - environment.id - ) - ) - ) - ]) - ) - }) - )(EditExerciseSimpleConfig) - ) +export default withLinks( + connect( + (state, { params: { exerciseId } }) => { + return { + exercise: getExercise(exerciseId)(state), + userId: loggedInUserIdSelector(state), + runtimeEnvironments: runtimeEnvironmentsSelector(state), + exerciseConfig: exerciseConfigSelector(exerciseId)(state), + exerciseEnvironmentConfig: exerciseEnvironmentConfigSelector( + exerciseId + )(state), + exerciseScoreConfig: exerciseScoreConfigSelector(exerciseId)(state), + exerciseTests: exerciseTestsSelector(exerciseId)(state), + pipelines: pipelinesSelector(state) + }; + }, + (dispatch, { params: { exerciseId } }) => ({ + loadAsync: () => + EditExerciseSimpleConfig.loadAsync({ exerciseId }, dispatch), + 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)), + reloadExercise: () => dispatch(fetchExercise(exerciseId)), + reloadConfig: () => + dispatch(fetchExercise(exerciseId)).then(({ value: exercise }) => + Promise.all([ + dispatch(fetchExerciseConfig(exerciseId)), + dispatch(fetchExerciseEnvironmentConfig(exerciseId)) + ]) + ) + }) + )(injectIntl(EditExerciseSimpleConfig)) ); diff --git a/src/pages/EditGroup/EditGroup.js b/src/pages/EditGroup/EditGroup.js index 0e50df632..414e6f0b0 100644 --- a/src/pages/EditGroup/EditGroup.js +++ b/src/pages/EditGroup/EditGroup.js @@ -19,7 +19,7 @@ import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { isSupervisorOf } from '../../redux/selectors/users'; import { getLocalizedTextsLocales } from '../../helpers/getLocalizedData'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class EditGroup extends Component { componentWillMount = () => this.props.loadAsync(); diff --git a/src/pages/EditInstance/EditInstance.js b/src/pages/EditInstance/EditInstance.js index 77d357156..a914d42c3 100644 --- a/src/pages/EditInstance/EditInstance.js +++ b/src/pages/EditInstance/EditInstance.js @@ -15,7 +15,7 @@ import { } from '../../redux/modules/instances'; import { instanceSelector } from '../../redux/selectors/instances'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class EditInstance extends Component { static loadAsync = ({ instanceId }, dispatch) => diff --git a/src/pages/EditPipeline/EditPipeline.js b/src/pages/EditPipeline/EditPipeline.js index 12ac8e8dd..76d24e34b 100644 --- a/src/pages/EditPipeline/EditPipeline.js +++ b/src/pages/EditPipeline/EditPipeline.js @@ -22,7 +22,7 @@ import { getPipeline } from '../../redux/selectors/pipelines'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { getBoxTypes } from '../../redux/selectors/boxes'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import { transformPipelineDataForApi, extractVariables diff --git a/src/pages/EmailVerification/EmailVerification.js b/src/pages/EmailVerification/EmailVerification.js index f8d2f51fb..d3fec6291 100644 --- a/src/pages/EmailVerification/EmailVerification.js +++ b/src/pages/EmailVerification/EmailVerification.js @@ -19,7 +19,7 @@ import { } from '../../redux/selectors/emailVerification'; import { LoadingIcon, SuccessIcon, FailedIcon } from '../../components/icons'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; /** * Component for changing old password for a new one for a user with a specific diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index 55ca0ed7d..d568f6aba 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -9,10 +9,11 @@ import { intlShape, injectIntl } from 'react-intl'; -import { Row, Col, ButtonGroup } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import Icon from 'react-fontawesome'; +import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer'; import Button from '../../components/widgets/FlatButton'; import Page from '../../components/layout/Page'; import ExerciseDetail from '../../components/Exercises/ExerciseDetail'; @@ -31,7 +32,7 @@ import { } from '../../components/icons'; import Confirm from '../../components/forms/Confirm'; import PipelinesSimpleList from '../../components/Pipelines/PipelinesSimpleList'; -// import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; +import ExerciseButtons from '../../components/Exercises/ExerciseButtons'; import AssignExerciseButton from '../../components/buttons/AssignExerciseButton'; import { isSubmitting } from '../../redux/selectors/submission'; @@ -48,7 +49,7 @@ import { fetchHardwareGroups } from '../../redux/modules/hwGroups'; import { create as assignExercise } from '../../redux/modules/assignments'; import { exerciseSelector } from '../../redux/selectors/exercises'; import { referenceSolutionsSelector } from '../../redux/selectors/referenceSolutions'; -import { canEditExercise } from '../../redux/selectors/users'; +import { canLoggedUserEditExercise } from '../../redux/selectors/users'; import { deletePipeline, fetchExercisePipelines, @@ -63,8 +64,7 @@ import { groupsSelector } from '../../redux/selectors/groups'; -import withLinks from '../../hoc/withLinks'; -import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer'; +import withLinks from '../../helpers/withLinks'; const messages = defineMessages({ groupsBox: { @@ -151,8 +151,6 @@ class Exercise extends Component { const { links: { EXERCISES_URI, - EXERCISE_EDIT_URI_FACTORY, - EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, EXERCISE_REFERENCE_SOLUTION_URI_FACTORY, PIPELINE_EDIT_URI_FACTORY } @@ -209,44 +207,8 @@ class Exercise extends Component {
} - {canEditExercise(exercise.id) && -
- - - - - - - - {/* forkExercise(forkId, formData)} - /> */} - -
} -

+ {canEditExercise && + } @@ -351,7 +313,7 @@ class Exercise extends Component { > {' '} @@ -426,7 +388,7 @@ class Exercise extends Component { > {' '} @@ -479,7 +441,7 @@ Exercise.propTypes = { push: PropTypes.func.isRequired, exercise: ImmutablePropTypes.map, supervisedGroups: PropTypes.object, - canEditExercise: PropTypes.func.isRequired, + canEditExercise: PropTypes.bool.isRequired, referenceSolutions: ImmutablePropTypes.map, intl: intlShape.isRequired, submitting: PropTypes.bool, @@ -493,38 +455,32 @@ Exercise.propTypes = { }; export default withLinks( - injectIntl( - connect( - (state, { params: { exerciseId } }) => { - const userId = loggedInUserIdSelector(state); + connect( + (state, { params: { exerciseId } }) => { + const userId = loggedInUserIdSelector(state); - return { - userId, - exercise: exerciseSelector(exerciseId)(state), - submitting: isSubmitting(state), - supervisedGroups: supervisorOfSelector(userId)(state), - canEditExercise: exerciseId => - canEditExercise(userId, exerciseId)(state), - referenceSolutions: referenceSolutionsSelector(exerciseId)(state), - exercisePipelines: exercisePipelinesSelector(exerciseId)(state), - groups: groupsSelector(state) - }; - }, - (dispatch, { params: { exerciseId } }) => ({ - loadAsync: userId => - Exercise.loadAsync({ exerciseId }, dispatch, userId), - assignExercise: groupId => - dispatch(assignExercise(groupId, exerciseId)), - push: url => dispatch(push(url)), - initCreateReferenceSolution: userId => - dispatch(init(userId, exerciseId)), - createExercisePipeline: () => - dispatch(createPipeline({ exerciseId: exerciseId })), - deleteReferenceSolution: (exerciseId, solutionId) => - dispatch(deleteReferenceSolution(exerciseId, solutionId)), - forkExercise: (forkId, data) => - dispatch(forkExercise(exerciseId, forkId, data)) - }) - )(Exercise) - ) + return { + userId, + exercise: exerciseSelector(exerciseId)(state), + submitting: isSubmitting(state), + supervisedGroups: supervisorOfSelector(userId)(state), + canEditExercise: canLoggedUserEditExercise(exerciseId)(state), + referenceSolutions: referenceSolutionsSelector(exerciseId)(state), + exercisePipelines: exercisePipelinesSelector(exerciseId)(state), + groups: groupsSelector(state) + }; + }, + (dispatch, { params: { exerciseId } }) => ({ + loadAsync: userId => Exercise.loadAsync({ exerciseId }, dispatch, userId), + assignExercise: groupId => dispatch(assignExercise(groupId, exerciseId)), + push: url => dispatch(push(url)), + initCreateReferenceSolution: userId => dispatch(init(userId, exerciseId)), + createExercisePipeline: () => + dispatch(createPipeline({ exerciseId: exerciseId })), + deleteReferenceSolution: (exerciseId, solutionId) => + dispatch(deleteReferenceSolution(exerciseId, solutionId)), + forkExercise: (forkId, data) => + dispatch(forkExercise(exerciseId, forkId, data)) + }) + )(injectIntl(Exercise)) ); diff --git a/src/pages/Exercises/Exercises.js b/src/pages/Exercises/Exercises.js index 988331d46..37e756376 100644 --- a/src/pages/Exercises/Exercises.js +++ b/src/pages/Exercises/Exercises.js @@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl'; import Button from '../../components/widgets/FlatButton'; import { push } from 'react-router-redux'; import { LinkContainer } from 'react-router-bootstrap'; -import { ButtonGroup } from 'react-bootstrap'; import DeleteExerciseButtonContainer from '../../containers/DeleteExerciseButtonContainer'; import SearchContainer from '../../containers/SearchContainer'; @@ -14,10 +13,9 @@ import Box from '../../components/widgets/Box'; import { AddIcon, EditIcon } from '../../components/icons'; import { fetchManyStatus } from '../../redux/selectors/exercises'; import { - canEditExercise, + canLoggedUserEditExercise, isLoggedAsSuperAdmin } from '../../redux/selectors/users'; -import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { fetchExercises, create as createExercise @@ -26,7 +24,7 @@ import { searchExercises } from '../../redux/modules/search'; import { getSearchQuery } from '../../redux/selectors/search'; import ExercisesList from '../../components/Exercises/ExercisesList'; import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Exercises extends Component { static loadAsync = (params, dispatch) => dispatch(fetchExercises()); @@ -55,7 +53,8 @@ class Exercises extends Component { search, links: { EXERCISE_EDIT_URI_FACTORY, - EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY + EXERCISE_EDIT_SIMPLE_CONFIG_URI_FACTORY, + EXERCISE_EDIT_LIMITS_URI_FACTORY } } = this.props; @@ -159,13 +158,13 @@ class Exercises extends Component { exercises={exercises} createActions={id => isAuthorOfExercise(id) && - +

@@ -176,16 +175,28 @@ class Exercises extends Component { {' '} + + + + search(query)} /> - } +
} />} /> @@ -210,13 +221,12 @@ Exercises.propTypes = { export default withLinks( connect( state => { - const userId = loggedInUserIdSelector(state); return { query: getSearchQuery('exercises-page')(state), fetchStatus: fetchManyStatus(state), isSuperAdmin: isLoggedAsSuperAdmin(state), isAuthorOfExercise: exerciseId => - canEditExercise(userId, exerciseId)(state) + canLoggedUserEditExercise(exerciseId)(state) }; }, dispatch => ({ diff --git a/src/pages/FAQ/FAQ.js b/src/pages/FAQ/FAQ.js index 12d851420..5b83fa3d6 100644 --- a/src/pages/FAQ/FAQ.js +++ b/src/pages/FAQ/FAQ.js @@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl'; import ReactMarkdown from 'react-remarkable'; import PageContent from '../../components/layout/PageContent'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; const FAQ_MD_URI = 'https://raw.githubusercontent.com/wiki/ReCodEx/wiki/FAQ.md'; diff --git a/src/pages/FeedbackAndBugs/FeedbackAndBugs.js b/src/pages/FeedbackAndBugs/FeedbackAndBugs.js index 0299a34db..c77721546 100644 --- a/src/pages/FeedbackAndBugs/FeedbackAndBugs.js +++ b/src/pages/FeedbackAndBugs/FeedbackAndBugs.js @@ -5,7 +5,7 @@ import { Row, Col } from 'react-bootstrap'; import Icon from 'react-fontawesome'; import PageContent from '../../components/layout/PageContent'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; const FeedbackAndBugs = ({ links: { GITHUB_BUGS_URL } }) => @@ -116,7 +116,7 @@ class Instance extends Component { collapsable={true} isOpen={true} formValues={formValues} - initialValues={EMPTY_OBJECT} + initialValues={EMPTY_OBJ} /> diff --git a/src/pages/Login/Login.js b/src/pages/Login/Login.js index c7a6c8832..35e060cf9 100644 --- a/src/pages/Login/Login.js +++ b/src/pages/Login/Login.js @@ -14,7 +14,7 @@ import CASLoginBox from '../../containers/CAS'; import { login } from '../../redux/modules/auth'; import { isLoggedIn } from '../../redux/selectors/auth'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Login extends Component { componentWillMount = () => { diff --git a/src/pages/Pipeline/Pipeline.js b/src/pages/Pipeline/Pipeline.js index ada43e8f1..b5686d4fb 100644 --- a/src/pages/Pipeline/Pipeline.js +++ b/src/pages/Pipeline/Pipeline.js @@ -23,7 +23,7 @@ import { fetchExercises } from '../../redux/modules/exercises'; import { exercisesSelector } from '../../redux/selectors/exercises'; import { createGraphFromNodes } from '../../helpers/pipelineGraph'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import PipelineDetail from '../../components/Pipelines/PipelineDetail'; import PipelineVisualisation from '../../components/Pipelines/PipelineVisualisation'; diff --git a/src/pages/Pipelines/Pipelines.js b/src/pages/Pipelines/Pipelines.js index 1c0a12516..0588f5ca6 100644 --- a/src/pages/Pipelines/Pipelines.js +++ b/src/pages/Pipelines/Pipelines.js @@ -23,7 +23,7 @@ import { searchPipelines } from '../../redux/modules/search'; import PipelinesList from '../../components/Pipelines/PipelinesList'; import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Pipelines extends Component { static loadAsync = (params, dispatch) => dispatch(fetchPipelines()); diff --git a/src/pages/ReferenceSolution/ReferenceSolution.js b/src/pages/ReferenceSolution/ReferenceSolution.js index a61b5335c..e78845750 100644 --- a/src/pages/ReferenceSolution/ReferenceSolution.js +++ b/src/pages/ReferenceSolution/ReferenceSolution.js @@ -10,7 +10,7 @@ import { import ImmutablePropTypes from 'react-immutable-proptypes'; import { Row, Col, Button } from 'react-bootstrap'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import Page from '../../components/layout/Page'; import { diff --git a/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js b/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js index 165bbe427..0d0730978 100644 --- a/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js +++ b/src/pages/ReferenceSolutionEvaluation/ReferenceSolutionEvaluation.js @@ -9,7 +9,7 @@ import { } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; import Page from '../../components/layout/Page'; import { fetchReferenceSolutionsIfNeeded } from '../../redux/modules/referenceSolutions'; diff --git a/src/pages/Registration/Registration.js b/src/pages/Registration/Registration.js index 45a6b25e6..788e346c1 100644 --- a/src/pages/Registration/Registration.js +++ b/src/pages/Registration/Registration.js @@ -19,7 +19,7 @@ import { fetchInstances } from '../../redux/modules/instances'; import { publicInstancesSelector } from '../../redux/selectors/instances'; import { hasSucceeded } from '../../redux/selectors/registration'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Register extends Component { componentWillMount = () => { diff --git a/src/pages/ResetPassword/ResetPassword.js b/src/pages/ResetPassword/ResetPassword.js index 117ce6b77..79703417c 100644 --- a/src/pages/ResetPassword/ResetPassword.js +++ b/src/pages/ResetPassword/ResetPassword.js @@ -15,7 +15,7 @@ import { hasResetingSucceeded as hasSucceeded } from '../../redux/selectors/auth'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; /** * This component enables the user to request reseting password for his/her email address. diff --git a/src/pages/SisIntegration/SisIntegration.js b/src/pages/SisIntegration/SisIntegration.js index 14053a217..297af95e8 100644 --- a/src/pages/SisIntegration/SisIntegration.js +++ b/src/pages/SisIntegration/SisIntegration.js @@ -119,7 +119,7 @@ class SisIntegration extends Component { > {' '} diff --git a/src/pages/User/User.js b/src/pages/User/User.js index b2c861b2f..2756cd094 100644 --- a/src/pages/User/User.js +++ b/src/pages/User/User.js @@ -38,7 +38,7 @@ import { import { InfoIcon } from '../../components/icons'; import { getJsData } from '../../redux/helpers/resourceManager'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class User extends Component { componentWillMount = () => diff --git a/src/pages/Users/Users.js b/src/pages/Users/Users.js index dd5d7689c..f422ad29b 100644 --- a/src/pages/Users/Users.js +++ b/src/pages/Users/Users.js @@ -23,7 +23,7 @@ import { fetchAllUsers } from '../../redux/modules/users'; import { takeOver } from '../../redux/modules/auth'; import { searchPeople } from '../../redux/modules/search'; -import withLinks from '../../hoc/withLinks'; +import withLinks from '../../helpers/withLinks'; class Users extends Component { static loadAsync = (params, dispatch) => dispatch(fetchAllUsers); diff --git a/src/pages/routes.js b/src/pages/routes.js index 1244b098a..6b9f4b8f7 100644 --- a/src/pages/routes.js +++ b/src/pages/routes.js @@ -15,6 +15,7 @@ import Exercises from './Exercises'; import EditExercise from './EditExercise'; import EditExerciseConfig from './EditExerciseConfig'; import EditExerciseSimpleConfig from './EditExerciseSimpleConfig'; +import EditExerciseLimits from './EditExerciseLimits'; import FeedbackAndBugs from './FeedbackAndBugs'; import Group from './Group'; import EditGroup from './EditGroup'; @@ -113,6 +114,7 @@ const createRoutes = getState => { path="edit-simple-config" component={EditExerciseSimpleConfig} /> + (dispatch, getState) => { const state = getState(); - const firstTestKey = encodeTestId(firstTestId); + const firstTestKey = encodeNumId(firstTestId); const template = getTestConfig(state, formName, firstTestKey); const transformations = prepareTransformations( template, @@ -288,8 +288,8 @@ export const smartFillExerciseConfigForm = ( files ); return Promise.all( - tests.filter(({ id }) => encodeTestId(id) !== firstTestKey).map(test => { - const testKey = encodeTestId(test.id); + tests.filter(({ id }) => encodeNumId(id) !== firstTestKey).map(test => { + const testKey = encodeNumId(test.id); return Promise.all( [ 'inputStdin', diff --git a/src/redux/modules/exercises.js b/src/redux/modules/exercises.js index b8df6ac34..0f41f24d2 100644 --- a/src/redux/modules/exercises.js +++ b/src/redux/modules/exercises.js @@ -1,37 +1,18 @@ -import { - handleActions -} from 'redux-actions'; -import { - Map, - List -} from 'immutable'; -import factory, { - initialState -} from '../helpers/resourceManager'; -import { - createApiAction -} from '../middleware/apiMiddleware'; +import { handleActions } from 'redux-actions'; +import { Map, List, fromJS } from 'immutable'; +import factory, { initialState } from '../helpers/resourceManager'; +import { createApiAction } from '../middleware/apiMiddleware'; -import { - actionTypes as supplementaryFilesActionTypes -} from './supplementaryFiles'; +import { actionTypes as supplementaryFilesActionTypes } from './supplementaryFiles'; -import { - actionTypes as attachmentFilesActionTypes -} from './attachmentFiles'; +import { actionTypes as attachmentFilesActionTypes } from './attachmentFiles'; const resourceName = 'exercises'; -const { - actions, - reduceActions, - actionTypes -} = factory({ +const { actions, reduceActions, actionTypes } = factory({ resourceName }); -export { - actionTypes -}; +export { actionTypes }; /** * Actions @@ -43,7 +24,10 @@ export const additionalActionTypes = { FORK_EXERCISE_PENDING: 'recodex/exercises/FORK_EXERCISE_PENDING', FORK_EXERCISE_REJECTED: 'recodex/exercises/FORK_EXERCISE_REJECTED', FORK_EXERCISE_FULFILLED: 'recodex/exercises/FORK_EXERCISE_FULFILLED', - GET_PIPELINE_VARIABLES: 'recodex/exercises/GET_PIPELINE_VARIABLES' + GET_PIPELINE_VARIABLES: 'recodex/exercises/GET_PIPELINE_VARIABLES', + SET_HARDWARE_GROUPS: 'recodex/exercises/SET_HARDWARE_GROUPS', + SET_HARDWARE_GROUPS_FULFILLED: + 'recodex/exercises/SET_HARDWARE_GROUPS_FULFILLED' }; export const loadExercise = actions.pushResource; @@ -109,12 +93,14 @@ export const getVariablesForPipelines = ( pipelinesIds ) => { const body = - runtimeEnvironmentId === 'default' ? { - pipelinesIds - } : { - runtimeEnvironmentId, - pipelinesIds - }; + runtimeEnvironmentId === 'default' + ? { + pipelinesIds + } + : { + runtimeEnvironmentId, + pipelinesIds + }; return createApiAction({ type: additionalActionTypes.GET_PIPELINE_VARIABLES, method: 'POST', @@ -128,6 +114,17 @@ export const getVariablesForPipelines = ( }); }; +export const setExerciseHardwareGroups = (id, hwGroups) => { + let actionData = { + type: additionalActionTypes.SET_HARDWARE_GROUPS, + endpoint: `/exercises/${id}/hardware-groups`, + method: 'POST', + meta: { id }, + body: { hwGroups } + }; + return createApiAction(actionData); +}; + /** * Reducer */ @@ -135,13 +132,9 @@ export const getVariablesForPipelines = ( const reducer = handleActions( Object.assign({}, reduceActions, { [additionalActionTypes.FORK_EXERCISE_PENDING]: ( - state, { - meta: { - id, - forkId - } - } - ) => + state, + { meta: { id, forkId } } + ) => state.updateIn(['resources', id, 'data'], exercise => { if (!exercise.has('forks')) { exercise = exercise.set('forks', Map()); @@ -155,54 +148,42 @@ const reducer = handleActions( }), [additionalActionTypes.FORK_EXERCISE_REJECTED]: ( - state, { - meta: { - id, - forkId - } - } - ) => + state, + { meta: { id, forkId } } + ) => state.setIn(['resources', id, 'data', 'forks', forkId], { status: forkStatuses.REJECTED }), [additionalActionTypes.FORK_EXERCISE_FULFILLED]: ( - state, { - payload: { - id: exerciseId - }, - meta: { - id, - forkId - } - } - ) => + state, + { payload: { id: exerciseId }, meta: { id, forkId } } + ) => state.setIn(['resources', id, 'data', 'forks', forkId], { status: forkStatuses.FULFILLED, exerciseId }), [supplementaryFilesActionTypes.ADD_FILES_FULFILLED]: ( - state, { - payload: files, - meta: { - exerciseId - } - } - ) => - state.hasIn(['resources', exerciseId]) ? - updateFiles(state, exerciseId, files, 'supplementaryFilesIds') : state, + state, + { payload: files, meta: { exerciseId } } + ) => + state.hasIn(['resources', exerciseId]) + ? updateFiles(state, exerciseId, files, 'supplementaryFilesIds') + : state, [attachmentFilesActionTypes.ADD_FILES_FULFILLED]: ( - state, { - payload: files, - meta: { - exerciseId - } - } - ) => - state.hasIn(['resources', exerciseId]) ? - updateFiles(state, exerciseId, files, 'attachmentFilesIds') : state + state, + { payload: files, meta: { exerciseId } } + ) => + state.hasIn(['resources', exerciseId]) + ? updateFiles(state, exerciseId, files, 'attachmentFilesIds') + : state, + + [additionalActionTypes.SET_HARDWARE_GROUPS_FULFILLED]: ( + state, + { payload, meta: { id } } + ) => state.setIn(['resources', id, 'data'], fromJS(payload)) }), initialState ); diff --git a/src/redux/modules/limits.js b/src/redux/modules/limits.js index 9991714bb..df8b95af3 100644 --- a/src/redux/modules/limits.js +++ b/src/redux/modules/limits.js @@ -1,4 +1,7 @@ import { handleActions } from 'redux-actions'; +import { change } from 'redux-form'; + +import { encodeId, encodeNumId } from '../../helpers/common'; import factory, { initialState } from '../helpers/resourceManager'; /** @@ -11,6 +14,12 @@ const { actions, reduceActions } = factory({ apiEndpointFactory: id => id }); +export const additionalActionTypes = { + CLONE_VERTICAL: 'recodex/limits/CLONE_VERTICAL', + CLONE_HORIZONTAL: 'recodex/limits/CLONE_HORIZONTAL', + CLONE_ALL: 'recodex/limits/CLONE_ALL' +}; + export const endpointDisguisedAsIdFactory = ({ exerciseId, hwGroup, @@ -47,5 +56,126 @@ export const editEnvironmentLimits = ( data ); +/* + * Special functions for cloning buttons + */ + +// Get a single value by its test name, environment ID, and field identifier +const getFormLimitsOf = ( + { form }, + formName, + testId, + runtimeEnvironmentId, + field +) => { + const testEnc = encodeNumId(testId); + const envEnc = encodeId(runtimeEnvironmentId); + return ( + form[formName].values.limits[testEnc][envEnc][field] || + form[formName].initial.limits[testId][envEnc][field] || + null + ); +}; + +// Lists all form keys to which the value should be copied +const getTargetFormKeys = ( + { form }, // form key in the store + formName, // form identifier + 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 time) +) => { + const testEnc = testId ? encodeNumId(testId) : null; + const envEnc = environmentId ? encodeId(environmentId) : null; + return form && form[formName] && form[formName].registeredFields + ? Object.keys(form[formName].registeredFields).filter(key => { + const [, test, env, f] = key.split('.'); + return ( + (!testEnc || test === testEnc) && + (!envEnc || env === envEnc) && + f === field + ); + }) + : []; +}; + +// Clone given value vertically (all test in environment) +export const cloneVertically = ( + formName, // form identifier + testId, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or time) +) => (dispatch, getState) => { + const state = getState(); + const value = getFormLimitsOf( + state, + formName, + testId, + runtimeEnvironmentId, + field + ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + null, // no test name => all test selected + runtimeEnvironmentId, + field + ).map(key => dispatch(change(formName, key, value))); + } +}; + +// Clone given value horizontally (all environments of the same test) +export const cloneHorizontally = ( + formName, // form identifier + testId, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or time) +) => (dispatch, getState) => { + const state = getState(); + const value = getFormLimitsOf( + state, + formName, + testId, + runtimeEnvironmentId, + field + ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + testId, + null, // no environemnt ID => all environments accepted + field + ).map(key => dispatch(change(formName, key, value))); + } +}; + +// Clone given value to all fields +export const cloneAll = ( + formName, // form identifier + testId, // test identifier + runtimeEnvironmentId, // environment identifier + field // field identifier (memory or time) +) => (dispatch, getState) => { + const state = getState(); + const value = getFormLimitsOf( + state, + formName, + testId, + runtimeEnvironmentId, + field + ); + if (value !== null) { + getTargetFormKeys( + state, + formName, + null, // no test name ... + null, // ... nor environemnt ID => all fields + field + ).map(key => dispatch(change(formName, key, value))); + } +}; + const reducer = handleActions(reduceActions, initialState); export default reducer; diff --git a/src/redux/modules/simpleLimits.js b/src/redux/modules/simpleLimits.js deleted file mode 100644 index a485c63fa..000000000 --- a/src/redux/modules/simpleLimits.js +++ /dev/null @@ -1,178 +0,0 @@ -import { handleActions } from 'redux-actions'; -import factory, { initialState } from '../helpers/resourceManager'; -import { change } from 'redux-form'; - -/** - * Create actions & reducer - */ - -const resourceName = 'simpleLimits'; -const { actions, reduceActions } = factory({ - resourceName, - apiEndpointFactory: id => id -}); - -export const additionalActionTypes = { - CLONE_VERTICAL: 'recodex/simpleLimits/CLONE_VERTICAL', - CLONE_HORIZONTAL: 'recodex/simpleLimits/CLONE_HORIZONTAL', - CLONE_ALL: 'recodex/simpleLimits/CLONE_ALL' -}; - -export const endpointDisguisedAsIdFactory = ({ - exerciseId, - runtimeEnvironmentId -}) => `/exercises/${exerciseId}/environment/${runtimeEnvironmentId}/limits`; - -export const fetchExerciseEnvironmentSimpleLimits = ( - exerciseId, - runtimeEnvironmentId -) => - actions.fetchResource( - endpointDisguisedAsIdFactory({ exerciseId, runtimeEnvironmentId }) - ); - -export const fetchExerciseEnvironmentSimpleLimitsIfNeeded = ( - exerciseId, - runtimeEnvironmentId -) => - actions.fetchOneIfNeeded( - endpointDisguisedAsIdFactory({ exerciseId, runtimeEnvironmentId }) - ); - -export const editEnvironmentSimpleLimits = ( - exerciseId, - runtimeEnvironmentId, - data -) => - actions.updateResource( - endpointDisguisedAsIdFactory({ exerciseId, runtimeEnvironmentId }), - data - ); - -/* - * Special functions for cloning buttons - */ - -// Encoding function which help us avoid problems with some characters in env ids (e.g., character '.'). -export const encodeTestId = testId => 'test' + testId; -export const encodeEnvironmentId = envId => 'env' + btoa(envId); - -// Get a single value by its test name, environment ID, and field identifier -const getSimpleLimitsOf = ( - { form }, - formName, - testId, - runtimeEnvironmentId, - field -) => { - const testEnc = encodeTestId(testId); - const envEnc = encodeEnvironmentId(runtimeEnvironmentId); - return ( - form[formName].values.limits[testEnc][envEnc][field] || - form[formName].initial.limits[testId][envEnc][field] || - null - ); -}; - -// Lists all form keys to which the value should be copied -const getTargetFormKeys = ( - { form }, // form key in the store - formName, // form identifier - 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 time) -) => { - const testEnc = testId ? encodeTestId(testId) : null; - const envEnc = environmentId ? encodeEnvironmentId(environmentId) : null; - return form && form[formName] && form[formName].registeredFields - ? Object.keys(form[formName].registeredFields).filter(key => { - const [, test, env, f] = key.split('.'); - return ( - (!testEnc || test === testEnc) && - (!envEnc || env === envEnc) && - f === field - ); - }) - : []; -}; - -// Clone given value vertically (all test in environment) -export const cloneVertically = ( - formName, // form identifier - testId, // test identifier - runtimeEnvironmentId, // environment identifier - field // field identifier (memory or time) -) => (dispatch, getState) => { - const state = getState(); - const value = getSimpleLimitsOf( - state, - formName, - testId, - runtimeEnvironmentId, - field - ); - if (value !== null) { - getTargetFormKeys( - state, - formName, - null, // no test name => all test selected - runtimeEnvironmentId, - field - ).map(key => dispatch(change(formName, key, value))); - } -}; - -// Clone given value horizontally (all environments of the same test) -export const cloneHorizontally = ( - formName, // form identifier - testId, // test identifier - runtimeEnvironmentId, // environment identifier - field // field identifier (memory or time) -) => (dispatch, getState) => { - const state = getState(); - const value = getSimpleLimitsOf( - state, - formName, - testId, - runtimeEnvironmentId, - field - ); - if (value !== null) { - getTargetFormKeys( - state, - formName, - testId, - null, // no environemnt ID => all environments accepted - field - ).map(key => dispatch(change(formName, key, value))); - } -}; - -// Clone given value to all fields -export const cloneAll = ( - formName, // form identifier - testId, // test identifier - runtimeEnvironmentId, // environment identifier - field // field identifier (memory or time) -) => (dispatch, getState) => { - const state = getState(); - const value = getSimpleLimitsOf( - state, - formName, - testId, - runtimeEnvironmentId, - field - ); - if (value !== null) { - getTargetFormKeys( - state, - formName, - null, // no test name ... - null, // ... nor environemnt ID => all fields - field - ).map(key => dispatch(change(formName, key, value))); - } -}; - -const reducer = handleActions(reduceActions, initialState); -export default reducer; diff --git a/src/redux/reducer.js b/src/redux/reducer.js index f5f5186ac..94c443962 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -23,7 +23,6 @@ import groupExercises from './modules/groupExercises'; import instances from './modules/instances'; import licences from './modules/licences'; import limits from './modules/limits'; -import simpleLimits from './modules/simpleLimits'; import notifications from './modules/notifications'; import { default as search } from './modules/search'; // because of a named export 'search' import sidebar from './modules/sidebar'; @@ -71,7 +70,6 @@ const createRecodexReducers = token => ({ instances, licences, limits, - simpleLimits, notifications, search, sidebar, diff --git a/src/redux/selectors/assignments.js b/src/redux/selectors/assignments.js index 3a81ae314..e7b1dd8d9 100644 --- a/src/redux/selectors/assignments.js +++ b/src/redux/selectors/assignments.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List } from 'immutable'; +import { EMPTY_LIST, EMPTY_ARRAY } from '../../helpers/common'; import { getSubmissions } from './submissions'; import { runtimeEnvironmentSelector } from './runtimeEnvironments'; @@ -12,8 +12,6 @@ export const getAssignment = createSelector( assignments => id => assignments.get(id) ); -const EMPTY_ARRAY = []; - export const assignmentEnvironmentsSelector = createSelector( [getAssignment, runtimeEnvironmentSelector], (assignmentSelector, envSelector) => id => { @@ -36,7 +34,7 @@ export const getUserSubmissions = (userId, assignmentId) => userId ]); if (!assignmentSubmissions) { - return List(); + return EMPTY_LIST; } return assignmentSubmissions.map(id => submissions.get(id)); diff --git a/src/redux/selectors/attachmentFiles.js b/src/redux/selectors/attachmentFiles.js index 12c9a4d18..5b555ab8b 100644 --- a/src/redux/selectors/attachmentFiles.js +++ b/src/redux/selectors/attachmentFiles.js @@ -1,10 +1,8 @@ import { createSelector, defaultMemoize } from 'reselect'; -import { Map } from 'immutable'; +import { EMPTY_MAP } from '../../helpers/common'; import { isReady } from '../helpers/resourceManager'; import { getExercise } from './exercises'; -const EMPTY_MAP = Map(); - export const attachmentFilesSelector = state => state.attachmentFiles.get('resources'); diff --git a/src/redux/selectors/exercises.js b/src/redux/selectors/exercises.js index 024291955..43bc1c30f 100644 --- a/src/redux/selectors/exercises.js +++ b/src/redux/selectors/exercises.js @@ -1,9 +1,9 @@ import { createSelector } from 'reselect'; +import { EMPTY_ARRAY } from '../../helpers/common'; import { isReady } from '../helpers/resourceManager'; import { fetchManyEndpoint } from '../modules/exercises'; const getParam = (state, id) => id; -const EMPTY_ARR = []; const getExercises = state => state.exercises; const getResources = exercises => exercises.get('resources'); @@ -43,7 +43,7 @@ export const getExercisesForGroup = createSelector( (exercises, groupExercises, groupId) => { const groupExIds = groupExercises[groupId] ? groupExercises[groupId] - : EMPTY_ARR; + : EMPTY_ARRAY; return exercises .filter(isReady) .filter( diff --git a/src/redux/selectors/groups.js b/src/redux/selectors/groups.js index 8ddd85615..92ac3f150 100644 --- a/src/redux/selectors/groups.js +++ b/src/redux/selectors/groups.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List, Map } from 'immutable'; +import { EMPTY_LIST, EMPTY_MAP, EMPTY_ARRAY } from '../../helpers/common'; import { studentOfGroupsIdsSelector, @@ -12,8 +12,6 @@ import { isReady, getId, getJsData } from '../helpers/resourceManager'; * Select groups part of the state */ const getParam = (state, id) => id; -const EMPTY_MAP = Map(); -const EMPTY_LIST = List(); export const groupsSelector = state => state.groups.get('resources'); @@ -81,7 +79,7 @@ const usersOfGroup = (type, groupId) => group => group && isReady(group) ? group.getIn(['data', 'privateData', type]) - : List() + : EMPTY_LIST ); export const studentsOfGroup = groupId => usersOfGroup('students', groupId); @@ -94,8 +92,8 @@ export const groupsAssignmentsIdsSelector = (id, type = 'public') => groupSelector(id), group => group && isReady(group) - ? group.getIn(['data', 'assignments', type]) || List() - : List() + ? group.getIn(['data', 'assignments', type]) || EMPTY_LIST + : EMPTY_LIST ); export const groupsPublicAssignmentsSelector = createSelector( @@ -127,7 +125,7 @@ const getGroupParentIds = (id, groups) => { ) : [data.get('id')]; } else { - return []; + return EMPTY_ARRAY; } }; diff --git a/src/redux/selectors/instances.js b/src/redux/selectors/instances.js index c7d358dd6..2728ecb9f 100644 --- a/src/redux/selectors/instances.js +++ b/src/redux/selectors/instances.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; import { List } from 'immutable'; +import { EMPTY_LIST } from '../../helpers/common'; import { isReady } from '../helpers/resourceManager'; import { loggedInUserSelector } from './users'; -const EMPTY_LIST = List(); - const getInstances = state => state.instances; const getResources = instances => instances.get('resources'); diff --git a/src/redux/selectors/limits.js b/src/redux/selectors/limits.js index 8e8021b28..fccd8f7c9 100644 --- a/src/redux/selectors/limits.js +++ b/src/redux/selectors/limits.js @@ -1,17 +1,7 @@ import { createSelector } from 'reselect'; -import { endpointDisguisedAsIdFactory } from '../modules/limits'; - const getLimits = state => state.limits; -export const limitsSelector = (exerciseId, runtimeEnvironmentId, hwGroup) => - createSelector(getLimits, limits => - limits.getIn([ - 'resources', - endpointDisguisedAsIdFactory({ - exerciseId, - runtimeEnvironmentId, - hwGroup - }) - ]) - ); +export const limitsSelector = createSelector(getLimits, limits => + limits.get('resources') +); diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js deleted file mode 100644 index b2aca995c..000000000 --- a/src/redux/selectors/simpleLimits.js +++ /dev/null @@ -1,7 +0,0 @@ -import { createSelector } from 'reselect'; - -const getLimits = state => state.simpleLimits; - -export const simpleLimitsSelector = createSelector(getLimits, limits => - limits.get('resources') -); diff --git a/src/redux/selectors/sisSupervisedCourses.js b/src/redux/selectors/sisSupervisedCourses.js index a9ecc42e0..c94ce3f82 100644 --- a/src/redux/selectors/sisSupervisedCourses.js +++ b/src/redux/selectors/sisSupervisedCourses.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; import { Map } from 'immutable'; +import { EMPTY_MAP } from '../../helpers/common'; import { loggedInUserIdSelector } from './auth'; const getResources = state => state.sisSupervisedCourses.get('resources'); @@ -7,8 +8,6 @@ const getResources = state => state.sisSupervisedCourses.get('resources'); const getSisStateTerms = state => state.sisStatus.getIn(['resources', 'status', 'data', 'terms']); -const EMPTY_MAP = Map(); - export const sisSupervisedCoursesSelector = createSelector( [getResources, getSisStateTerms, loggedInUserIdSelector], (resources, terms, userId) => { diff --git a/src/redux/selectors/stats.js b/src/redux/selectors/stats.js index 9902f6f81..f2c7c7e9d 100644 --- a/src/redux/selectors/stats.js +++ b/src/redux/selectors/stats.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List } from 'immutable'; +import { EMPTY_LIST, EMPTY_OBJ } from '../../helpers/common'; import { getJsData } from '../helpers/resourceManager'; import { loggedInUserIdSelector } from './auth'; @@ -8,8 +8,6 @@ import { loggedInUserIdSelector } from './auth'; */ const getParam = (state, id) => id; -const EMPTY_LIST = List(); -const EMPTY_OBJ = {}; export const statisticsSelector = state => state.stats.get('resources'); @@ -29,7 +27,7 @@ export const getUsersStatistics = (groupId, userId) => export const getStatuses = (groupId, userId) => createSelector( getUsersStatistics(groupId, userId), - stats => (stats ? stats.statuses : {}) + stats => (stats ? stats.statuses : EMPTY_OBJ) ); export const getStatusesForLoggedUser = createSelector( diff --git a/src/redux/selectors/supplementaryFiles.js b/src/redux/selectors/supplementaryFiles.js index e52713ea6..904a00e70 100644 --- a/src/redux/selectors/supplementaryFiles.js +++ b/src/redux/selectors/supplementaryFiles.js @@ -1,10 +1,9 @@ import { createSelector, defaultMemoize } from 'reselect'; -import { Map } from 'immutable'; + +import { EMPTY_MAP } from '../../helpers/common'; import { isReady } from '../helpers/resourceManager'; import { getExercise } from './exercises'; -const EMPTY_MAP = Map(); - export const supplementaryFilesSelector = state => state.supplementaryFiles.get('resources'); diff --git a/src/redux/selectors/users.js b/src/redux/selectors/users.js index 3dc8aafab..5353afdbb 100644 --- a/src/redux/selectors/users.js +++ b/src/redux/selectors/users.js @@ -1,5 +1,7 @@ import { createSelector } from 'reselect'; import { List } from 'immutable'; + +import { EMPTY_LIST, EMPTY_OBJ } from '../../helpers/common'; import { fetchManyEndpoint } from '../modules/users'; import { extractLanguageFromUrl } from '../../links'; @@ -15,7 +17,6 @@ import { pipelineSelector } from './pipelines'; import { isReady, getJsData } from '../helpers/resourceManager'; const getParam = (state, id) => id; -const EMPTY_LIST = List(); const getUsers = state => state.users; const getResources = users => users.get('resources'); @@ -95,7 +96,7 @@ export const getUserSettings = userId => user => isReady(user) ? user.getIn(['data', 'privateData', 'settings']).toJS() - : {} + : EMPTY_OBJ ); export const loggedInUserSelector = createSelector( @@ -117,7 +118,7 @@ export const memberOfInstancesIdsSelector = userId => user => user && isReady(user) ? List([user.getIn(['data', 'privateData', 'instanceId'])]) - : List() // @todo: Change when the user can be member of multiple instances + : EMPTY_LIST // @todo: Change when the user can be member of multiple instances ); export const studentOfGroupsIdsSelector = userId => @@ -125,8 +126,8 @@ export const studentOfGroupsIdsSelector = userId => getUser(userId), user => user && isReady(user) - ? user.getIn(['data', 'privateData', 'groups', 'studentOf'], List()) - : List() + ? user.getIn(['data', 'privateData', 'groups', 'studentOf'], EMPTY_LIST) + : EMPTY_LIST ); export const supervisorOfGroupsIdsSelector = userId => @@ -134,8 +135,11 @@ export const supervisorOfGroupsIdsSelector = userId => getUser(userId), user => user && isReady(user) - ? user.getIn(['data', 'privateData', 'groups', 'supervisorOf'], List()) - : List() + ? user.getIn( + ['data', 'privateData', 'groups', 'supervisorOf'], + EMPTY_LIST + ) + : EMPTY_LIST ); export const isStudentOf = (userId, groupId) => @@ -178,14 +182,20 @@ export const usersGroupsIds = userId => (student, supervisor) => student.concat(supervisor) ); -export const canEditExercise = (userId, exerciseId) => +export const canLoggedUserEditExercise = exerciseId => createSelector( - [exerciseSelector(exerciseId), isLoggedAsSuperAdmin], - (exercise, isSuperAdmin) => - isSuperAdmin || - (exercise && - isReady(exercise) && - exercise.getIn(['data', 'authorId']) === userId) + [ + exerciseSelector(exerciseId), + loggedInUserIdSelector, + isLoggedAsSuperAdmin + ], + (exercise, userId, isSuperAdmin) => + Boolean( + isSuperAdmin || + (exercise && + isReady(exercise) && + exercise.getIn(['data', 'authorId']) === userId) + ) ); export const canEditPipeline = (userId, pipelineId) => @@ -212,5 +222,5 @@ export const notificationsSelector = createSelector( }), {} ) - : {} + : EMPTY_OBJ ); diff --git a/src/redux/selectors/usersGroups.js b/src/redux/selectors/usersGroups.js index 13b374bbd..40d3e8b47 100644 --- a/src/redux/selectors/usersGroups.js +++ b/src/redux/selectors/usersGroups.js @@ -1,14 +1,12 @@ import { createSelector } from 'reselect'; -import { List, Map } from 'immutable'; +import { Map } from 'immutable'; +import { EMPTY_MAP, EMPTY_LIST } from '../../helpers/common'; import { loggedInUserSelector } from './users'; import { groupsSelector, filterGroups } from './groups'; import { getAssignments } from './assignments'; import { isReady } from '../helpers/resourceManager'; -const EMPTY_MAP = Map(); -const EMPTY_LIST = List(); - // all the groups in the state export const allGroups = state => state.groups; @@ -27,7 +25,7 @@ export const loggedInStudentOfGroupsIdsSelector = createSelector( user => user && isReady(user) ? user.getIn(['data', 'privateData', 'groups', 'studentOf']) - : List() + : EMPTY_LIST ); export const loggedInSupervisorOfGroupsIdsSelector = createSelector( @@ -35,7 +33,7 @@ export const loggedInSupervisorOfGroupsIdsSelector = createSelector( user => user && isReady(user) ? user.getIn(['data', 'privateData', 'groups', 'supervisorOf']) - : List() + : EMPTY_LIST ); export const loggedInStudentOfSelector = createSelector( diff --git a/views/index.ejs b/views/index.ejs index c1e430a85..33ba37d77 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -9,6 +9,19 @@