From 39da2657c1b418c102f130ec9f2b5b2c582983af Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 22 Feb 2018 15:53:21 +0100 Subject: [PATCH 1/4] Only superadmin can edit group external identifier --- .../forms/EditGroupForm/EditGroupForm.js | 31 ++++++++++--------- src/pages/EditGroup/EditGroup.js | 13 ++++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/components/forms/EditGroupForm/EditGroupForm.js b/src/components/forms/EditGroupForm/EditGroupForm.js index 29abfbf04..2495d6248 100644 --- a/src/components/forms/EditGroupForm/EditGroupForm.js +++ b/src/components/forms/EditGroupForm/EditGroupForm.js @@ -21,7 +21,8 @@ const EditGroupForm = ({ hasThreshold, collapsable = false, isOpen = true, - reset + reset, + isSuperAdmin }) => - - } - /> + {isSuperAdmin && + + } + />} { diff --git a/src/pages/EditGroup/EditGroup.js b/src/pages/EditGroup/EditGroup.js index 0e50df632..86e187330 100644 --- a/src/pages/EditGroup/EditGroup.js +++ b/src/pages/EditGroup/EditGroup.js @@ -16,7 +16,10 @@ import { LocalizedGroupName } from '../../components/helpers/LocalizedNames'; import { fetchGroupIfNeeded, editGroup } from '../../redux/modules/groups'; import { groupSelector } from '../../redux/selectors/groups'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; -import { isSupervisorOf } from '../../redux/selectors/users'; +import { + isSupervisorOf, + isLoggedAsSuperAdmin +} from '../../redux/selectors/users'; import { getLocalizedTextsLocales } from '../../helpers/getLocalizedData'; import withLinks from '../../hoc/withLinks'; @@ -50,6 +53,7 @@ class EditGroup extends Component { const { params: { groupId }, group, + isSuperAdmin, links: { GROUP_URI_FACTORY }, editGroup, hasThreshold, @@ -91,6 +95,7 @@ class EditGroup extends Component { onSubmit={editGroup} hasThreshold={hasThreshold} localizedTextsLocales={getLocalizedTextsLocales(localizedTexts)} + isSuperAdmin={isSuperAdmin} /> isSupervisorOf(userId, groupId)(state), hasThreshold: editGroupFormSelector(state, 'hasThreshold'), - localizedTexts: editGroupFormSelector(state, 'localizedTexts') + localizedTexts: editGroupFormSelector(state, 'localizedTexts'), + isSuperAdmin: isLoggedAsSuperAdmin(state) }; }, (dispatch, { params: { groupId } }) => ({ From 819362a50a1d74eaafe0b135d2a0f714320418e1 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Thu, 22 Feb 2018 16:49:48 +0100 Subject: [PATCH 2/4] Forking of exercises for superadmins --- .../ForkExerciseForm/ForkExerciseForm.css | 4 +- .../ForkExerciseForm/ForkExerciseForm.js | 15 ++++-- src/pages/Exercise/Exercise.js | 53 ++++++++++++++----- src/redux/modules/users.js | 1 + 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/components/forms/ForkExerciseForm/ForkExerciseForm.css b/src/components/forms/ForkExerciseForm/ForkExerciseForm.css index ca4e570d0..824f8b236 100644 --- a/src/components/forms/ForkExerciseForm/ForkExerciseForm.css +++ b/src/components/forms/ForkExerciseForm/ForkExerciseForm.css @@ -1,4 +1,4 @@ -.formSpace { - padding-left: 10px; +.forkForm { + padding-left: 0px; display: flex; } diff --git a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js index 20119434d..672fd4634 100644 --- a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js +++ b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js @@ -13,7 +13,7 @@ import { SuccessIcon } from '../../../components/icons'; import { forkStatuses } from '../../../redux/modules/exercises'; import { getFork } from '../../../redux/selectors/exercises'; import ResourceRenderer from '../../helpers/ResourceRenderer'; -import { getLocalizedName } from '../../../helpers/getLocalizedData'; +import { getGroupCanonicalLocalizedName } from '../../../helpers/getLocalizedData'; import withLinks from '../../../hoc/withLinks'; @@ -41,6 +41,7 @@ class ForkExerciseForm extends Component { hasSucceeded = false, invalid, groups, + groupsAccessor, intl: { locale } } = this.props; @@ -70,7 +71,7 @@ class ForkExerciseForm extends Component { defaultMessage="Saving failed. Please try again later." /> } -
+ {(...groups) => a.name.localeCompare(b.name, locale)) - .filter((item, pos, arr) => arr.indexOf(item) === pos) .map(group => ({ key: group.id, - name: getLocalizedName(group, locale) + name: getGroupCanonicalLocalizedName( + group, + groupsAccessor, + locale + ) })) + .sort((a, b) => a.name.localeCompare(b.name, locale)) )} />} @@ -140,6 +144,7 @@ ForkExerciseForm.propTypes = { push: PropTypes.func.isRequired, links: PropTypes.object, groups: ImmutablePropTypes.map, + groupsAccessor: PropTypes.func.isRequired, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index 55ca0ed7d..5901e44b5 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -31,7 +31,7 @@ import { } from '../../components/icons'; import Confirm from '../../components/forms/Confirm'; import PipelinesSimpleList from '../../components/Pipelines/PipelinesSimpleList'; -// import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; +import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; import AssignExerciseButton from '../../components/buttons/AssignExerciseButton'; import { isSubmitting } from '../../redux/selectors/submission'; @@ -48,23 +48,31 @@ 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 { + canEditExercise, + isLoggedAsSuperAdmin +} from '../../redux/selectors/users'; import { deletePipeline, fetchExercisePipelines, create as createPipeline } from '../../redux/modules/pipelines'; import { exercisePipelinesSelector } from '../../redux/selectors/pipelines'; -import { fetchUsersGroupsIfNeeded } from '../../redux/modules/groups'; +import { + fetchUsersGroupsIfNeeded, + fetchInstanceGroups +} from '../../redux/modules/groups'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { supervisorOfSelector, - groupsSelector + groupsSelector, + groupDataAccessorSelector } from '../../redux/selectors/groups'; import withLinks from '../../hoc/withLinks'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer'; +import { fetchUser } from '../../redux/modules/users'; const messages = defineMessages({ groupsBox: { @@ -90,7 +98,15 @@ class Exercise extends Component { dispatch(fetchReferenceSolutionsIfNeeded(exerciseId)), dispatch(fetchHardwareGroups()), dispatch(fetchExercisePipelines(exerciseId)), - dispatch(fetchUsersGroupsIfNeeded(userId)) + dispatch(fetchUsersGroupsIfNeeded(userId)), + dispatch(fetchUser(userId)) + .then(res => res.value) + .then( + data => + data.privateData.role === 'superadmin' + ? dispatch(fetchInstanceGroups(data.privateData.instanceId)) + : Promise.resolve() + ) ]); componentWillMount() { @@ -141,12 +157,14 @@ class Exercise extends Component { initCreateReferenceSolution, exercisePipelines, deleteReferenceSolution, - push - // groups, - // forkExercise + push, + groups, + groupsAccessor, + forkExercise, + isSuperAdmin } = this.props; - // const { forkId } = this.state; + const { forkId } = this.state; const { links: { @@ -238,13 +256,16 @@ class Exercise extends Component { /> - {/* +

+ {isSuperAdmin && + forkExercise(forkId, formData)} - /> */} - + groupsAccessor={groupsAccessor} + />} }

@@ -489,7 +510,9 @@ Exercise.propTypes = { links: PropTypes.object, deleteReferenceSolution: PropTypes.func.isRequired, forkExercise: PropTypes.func.isRequired, - groups: ImmutablePropTypes.map + groups: ImmutablePropTypes.map, + isSuperAdmin: PropTypes.bool, + groupsAccessor: PropTypes.func.isRequired }; export default withLinks( @@ -507,7 +530,9 @@ export default withLinks( canEditExercise(userId, exerciseId)(state), referenceSolutions: referenceSolutionsSelector(exerciseId)(state), exercisePipelines: exercisePipelinesSelector(exerciseId)(state), - groups: groupsSelector(state) + groups: groupsSelector(state), + groupsAccessor: groupDataAccessorSelector(state), + isSuperAdmin: isLoggedAsSuperAdmin(state) }; }, (dispatch, { params: { exerciseId } }) => ({ diff --git a/src/redux/modules/users.js b/src/redux/modules/users.js index ca60a576d..19618a918 100644 --- a/src/redux/modules/users.js +++ b/src/redux/modules/users.js @@ -45,6 +45,7 @@ export const fetchAllUsers = actions.fetchMany({ endpoint: fetchManyEndpoint }); export const loadUserData = actions.pushResource; +export const fetchUser = actions.fetchResource; export const fetchUserIfNeeded = actions.fetchIfNeeded; export const validateRegistrationData = (email, password) => createApiAction({ From ea22e875bec1c1492137ad2a661a3776ca289975 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Fri, 23 Feb 2018 22:55:09 +0100 Subject: [PATCH 3/4] Assign exercise in multiple groups --- .../forms/MultiAssignForm/MultiAssignForm.js | 350 ++++++++++++++++++ src/components/forms/MultiAssignForm/index.js | 1 + src/locales/cs.json | 107 ++++++ src/locales/en.json | 109 +++++- src/pages/Exercise/Exercise.js | 96 +++-- 5 files changed, 630 insertions(+), 33 deletions(-) create mode 100644 src/components/forms/MultiAssignForm/MultiAssignForm.js create mode 100644 src/components/forms/MultiAssignForm/index.js diff --git a/src/components/forms/MultiAssignForm/MultiAssignForm.js b/src/components/forms/MultiAssignForm/MultiAssignForm.js new file mode 100644 index 000000000..d6110ad59 --- /dev/null +++ b/src/components/forms/MultiAssignForm/MultiAssignForm.js @@ -0,0 +1,350 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { reduxForm, Field } from 'redux-form'; +import { FormattedMessage } from 'react-intl'; +import { Alert, HelpBlock } from 'react-bootstrap'; +import isNumeric from 'validator/lib/isNumeric'; + +import { DatetimeField, TextField, CheckboxField } from '../Fields'; +import SubmitButton from '../SubmitButton'; +import { getGroupCanonicalLocalizedName } from '../../../helpers/getLocalizedData'; + +const MultiAssignForm = ({ + anyTouched, + submitting, + handleSubmit, + submitFailed: hasFailed, + submitSucceeded: hasSucceeded, + asyncValidating, + invalid, + firstDeadline, + allowSecondDeadline, + groups, + groupsAccessor, + locale +}) => +

+ {hasFailed && + + + } + + {groups + .sort((a, b) => + getGroupCanonicalLocalizedName(a, groupsAccessor, locale).localeCompare( + getGroupCanonicalLocalizedName(b, groupsAccessor, locale), + locale + ) + ) + .map(group => + + )} + +
+ + + } + /> + + + } + /> + + + } + /> + + {allowSecondDeadline && + date.isSameOrAfter(firstDeadline)} + component={DatetimeField} + label={ + + } + />} + + {allowSecondDeadline && + !firstDeadline && + + + } + + {allowSecondDeadline && + + } + />} + + + } + /> + + + } + /> + + + } + /> + + + } + /> + +
+ + ), + submitting: ( + + ), + success: ( + + ) + }} + /> +
+
; + +MultiAssignForm.propTypes = { + initialValues: PropTypes.object, + values: PropTypes.object, + handleSubmit: PropTypes.func.isRequired, + anyTouched: PropTypes.bool, + submitting: PropTypes.bool, + submitFailed: PropTypes.bool, + submitSucceeded: PropTypes.bool, + asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + invalid: PropTypes.bool, + firstDeadline: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), // object == moment.js instance + allowSecondDeadline: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + groups: PropTypes.array.isRequired, + groupsAccessor: PropTypes.func.isRequired, + locale: PropTypes.string.isRequired +}; + +const isNonNegativeInteger = n => + typeof n !== 'undefined' && + (typeof n === 'number' || isNumeric(n)) && + parseInt(n) >= 0; + +const isPositiveInteger = n => + typeof n !== 'undefined' && + (typeof n === 'number' || isNumeric(n)) && + parseInt(n) > 0; + +const validate = ({ + submissionsCountLimit, + firstDeadline, + secondDeadline, + allowSecondDeadline, + maxPointsBeforeFirstDeadline, + maxPointsBeforeSecondDeadline, + pointsPercentualThreshold, + groups +}) => { + const errors = {}; + + if ( + !groups || + Object.keys(groups).length === 0 || + Object.values(groups).filter(val => val).length < 1 + ) { + errors['_error'] = ( + + ); + } + + if (!firstDeadline) { + errors['firstDeadline'] = ( + + ); + } + + if (!isPositiveInteger(submissionsCountLimit)) { + errors['submissionsCountLimit'] = ( + + ); + } + + if (!isNonNegativeInteger(maxPointsBeforeFirstDeadline)) { + errors['maxPointsBeforeFirstDeadline'] = ( + + ); + } + + if (allowSecondDeadline && !secondDeadline) { + errors['secondDeadline'] = ( + + ); + } + + if ( + allowSecondDeadline && + firstDeadline && + secondDeadline && + !firstDeadline.isSameOrBefore(secondDeadline) && + !firstDeadline.isSameOrBefore(secondDeadline, 'hour') + ) { + errors['secondDeadline'] = ( + + ); + } + + if ( + allowSecondDeadline && + !isNonNegativeInteger(maxPointsBeforeSecondDeadline) + ) { + errors['maxPointsBeforeSecondDeadline'] = ( + + ); + } + + if (pointsPercentualThreshold) { + const numericThreshold = Number(pointsPercentualThreshold); + if ( + pointsPercentualThreshold.toString() !== + Math.round(numericThreshold).toString() + ) { + errors['pointsPercentualThreshold'] = ( + + ); + } else if (numericThreshold < 0 || numericThreshold > 100) { + errors['pointsPercentualThreshold'] = ( + + ); + } + } + + return errors; +}; + +export default reduxForm({ + form: 'multiAssign', + validate, + enableReinitialize: true, + keepDirtyOnReinitialize: false +})(MultiAssignForm); diff --git a/src/components/forms/MultiAssignForm/index.js b/src/components/forms/MultiAssignForm/index.js new file mode 100644 index 000000000..8a38d3ec5 --- /dev/null +++ b/src/components/forms/MultiAssignForm/index.js @@ -0,0 +1 @@ +export default from './MultiAssignForm'; diff --git a/src/locales/cs.json b/src/locales/cs.json index 74c554b07..82af548f3 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -153,12 +153,22 @@ "app.confirm.no": "Ne", "app.confirm.yes": "Ano", "app.createGroup.externalId": "Externí identifikátor skupiny (například ID ze školního informačního systému):", + "app.createGroup.groupDescription": "Description:", + "app.createGroup.groupName": "Name:", "app.createGroup.hasThreshold": "Studenti potřebují určitý počet bodů pro splnění kurzu", "app.createGroup.isPublic": "Veřejná (skupinu vidí všichni uživatelé a můžou se do ní přidat)", "app.createGroup.publicStats": "Studenti mohou vidět dosažené body ostatních", "app.createGroup.threshold": "Minimální procentuální hranice potřebná ke splnění tohoto kurzu:", + "app.createGroup.validation.emptyDescription": "Group description cannot be empty.", + "app.createGroup.validation.emptyName": "Group name cannot be empty.", + "app.createGroup.validation.nameCollision": "The name \"{name}\" is already used, please choose a different one.", "app.createGroup.validation.thresholdBetweenZeroHundred": "Procentuální hranice musí být celé číslo od 0 do 100.", "app.createGroup.validation.thresholdMustBeInteger": "Procentuální hranice musí být celé číslo.", + "app.createGroupForm.createGroup": "Create new group", + "app.createGroupForm.failed": "We are sorry but we weren't able to create a new group.", + "app.createGroupForm.processing": "Group is being created ...", + "app.createGroupForm.success": "Group has been created", + "app.createGroupForm.title": "Create new group", "app.createGroupForm.validation.noLocalizedText": "Please add at least one localized text describing the group.", "app.dashboard.sisGroupsStudent": "SIS Courses - Student", "app.dashboard.sisGroupsStudentExplain": "SIS courses you are enrolled to in particular semesters and which have correspondig groups in ReCodEx.", @@ -175,6 +185,7 @@ "app.editAssignment.deleteAssignmentWarning": "Smazání zadané úlohy odstraní všechna studentská řešní. Pro případné obnovení těchto dat prosím kontaktujte správce ReCodExu.", "app.editAssignment.description": "Změnit nastavení zadání úlohy včetně jejích limitů", "app.editAssignment.title": "Upravit zadání úlohy", + "app.editAssignmentForm.addLanguage": "Add language variant", "app.editAssignmentForm.allowSecondDeadline": "Povolit druhý termín odevzdání.", "app.editAssignmentForm.canViewLimitRatios": "Viditelnost poměrů dosažených výsledků vůči limitům", "app.editAssignmentForm.chooseFirstDeadlineBeforeSecondDeadline": "Před nastavením druhého termínu odevzdání je nutné zvolit první termín.", @@ -186,9 +197,16 @@ "app.editAssignmentForm.localized.assignment": "Zadání úlohy a popis pro studenty:", "app.editAssignmentForm.localized.locale": "Jazyková mutace:", "app.editAssignmentForm.localized.name": "Name:", + "app.editAssignmentForm.localized.noLanguage": "There is currently no text in any language for this assignment.", + "app.editAssignmentForm.localized.reallyRemoveQuestion": "Do you really want to delete the assignmenet in this language?", + "app.editAssignmentForm.localized.remove": "Remove this language", "app.editAssignmentForm.maxPointsBeforeFirstDeadline": "Maximální počet bodů pro řešení odevzdaná před prvním termínem:", "app.editAssignmentForm.maxPointsBeforeSecondDeadline": "Maximální počet bodů pro řešení odevzdaná před druhým termínem:", + "app.editAssignmentForm.moreAboutScoreConfig": "Read more about score configuration syntax.", + "app.editAssignmentForm.name": "Assignment default name:", + "app.editAssignmentForm.newLocale": "New language", "app.editAssignmentForm.pointsPercentualThreshold": "Minimální procentuální správnost řešení, za kterou lze získat nějaké body:", + "app.editAssignmentForm.scoreConfig": "Score configuration:", "app.editAssignmentForm.secondDeadline": "Druhý termín odevzdání:", "app.editAssignmentForm.submissionsCountLimit": "Počet pokusů odevzdání:", "app.editAssignmentForm.submit": "Upravit nastavení", @@ -235,6 +253,13 @@ "app.editEnvironmentConfigVariables.stringArrayType": "Array of strings", "app.editEnvironmentConfigVariables.stringType": "Řetězec", "app.editEnvironmentConfigVariables.variables": "Proměnné:", + "app.editEnvironmentLimitsForm.box": "Box", + "app.editEnvironmentLimitsForm.environment.name": "Environment name:", + "app.editEnvironmentLimitsForm.environment.noEnvironment": "There is currently no environment specified for this assignment.", + "app.editEnvironmentLimitsForm.newEnvironment": "New environment", + "app.editEnvironmentLimitsForm.noBoxesForPipeline": "There are no boxes which need to set limits in this pipeline.", + "app.editEnvironmentLimitsForm.noPipelinesForTest": "There are no pipelines for this test to edit.", + "app.editEnvironmentLimitsForm.pipeline": "Pipeline", "app.editEnvironmentLimitsForm.submit": "Save changes to {env}", "app.editEnvironmentLimitsForm.submitting": "Saving changes ...", "app.editEnvironmentLimitsForm.success": "Saved.", @@ -269,10 +294,12 @@ "app.editExerciseConfigEnvironment.reallyRemoveQuestion": "Opravdu chcete smazat tuto konfiguraci prostředí?", "app.editExerciseConfigForm.addTest": "Přidat nový test", "app.editExerciseConfigForm.failed": "Uložení se nezdařilo. Prosíme opakujte akci později.", + "app.editExerciseConfigForm.fileType": "File", "app.editExerciseConfigForm.pipelines": "Pipeliny", "app.editExerciseConfigForm.removeLastTest": "Odstranit poslední test", "app.editExerciseConfigForm.smartFill": "Smart Fill", "app.editExerciseConfigForm.smartFill.yesNoQuestion": "Do you really wish to overwrite configuration of all subsequent tests using the first test as a template? Files will be paired to individual test configurations by a heuristics based on matching name substrings.", + "app.editExerciseConfigForm.stringType": "String", "app.editExerciseConfigForm.submit": "Změnit konfiguraci", "app.editExerciseConfigForm.submitting": "Ukládání konfigurace ...", "app.editExerciseConfigForm.success": "Konfigurace byla uložena.", @@ -280,6 +307,7 @@ "app.editExerciseConfigForm.validation.duplicatePipeline": "Please select a different pipeline.", "app.editExerciseConfigForm.validation.noEnvironments": "Please add at least one environment config for the exercise.", "app.editExerciseConfigForm.variables": "Proměnné", + "app.editExerciseForm.description": "Description for supervisors:", "app.editExerciseForm.difficulty": "Obtížnost", "app.editExerciseForm.easy": "Snadné", "app.editExerciseForm.failed": "Uložení se nezdařilo. Prosim, opakujte akci později.", @@ -288,6 +316,7 @@ "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", "app.editExerciseForm.isPublic": "Úloha je veřejná a může být použita cvičícími.", "app.editExerciseForm.medium": "Průměrné", + "app.editExerciseForm.name": "Exercise name:", "app.editExerciseForm.submit": "Upravit nastavení", "app.editExerciseForm.submitting": "Ukládání změn ...", "app.editExerciseForm.success": "Nastavení bylo uloženo.", @@ -302,6 +331,24 @@ "app.editExerciseForm.validation.noLocalizedText": "Prosíme přidejte alespoň jeden lokalizovaný text popisující tuto úlohu.", "app.editExerciseForm.validation.sameLocalizedTexts": "Je vyplněno více jazykových variant pro jednu lokalizaci. Prosím ujistěte se, že lokalizace jsou unikátní.", "app.editExerciseForm.validation.versionDiffers": "Někdo změnil tuto úlohu v průběhu její editace. Prosíme obnovte si tuto stránku a proveďte své změny znovu.", + "app.editExerciseLimitsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseLimitsForm.submit": "Change limits", + "app.editExerciseLimitsForm.submitting": "Saving limits ...", + "app.editExerciseLimitsForm.success": "Limits were saved.", + "app.editExerciseLimitsForm.validation.envName": "Please fill environment name.", + "app.editExerciseLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editExerciseLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editExerciseLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editExerciseLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editExerciseLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", + "app.editExerciseRuntimeConfigsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseRuntimeConfigsForm.submit": "Change runtime configurations", + "app.editExerciseRuntimeConfigsForm.submitting": "Saving runtime configurations ...", + "app.editExerciseRuntimeConfigsForm.success": "Runtime configurations were saved.", + "app.editExerciseRuntimeConfigsForm.validation.empty": "Please fill the runtime environment information.", + "app.editExerciseRuntimeConfigsForm.validation.jobConfig": "Please fill the job configuration of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.name": "Please fill the display name of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.runtimeEnvironmentId": "Please select a runtime environment.", "app.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í", @@ -343,6 +390,7 @@ "app.editGroupForm.set": "Upravit skupinu", "app.editGroupForm.success": "Nastavení skupiny bylo uloženo.", "app.editGroupForm.successNew": "Create group", + "app.editGroupForm.title": "Edit group", "app.editGroupForm.titleEdit": "Edit group", "app.editGroupForm.titleNew": "Create new group", "app.editGroupForm.validation.emptyName": "Please fill the name of the group.", @@ -385,6 +433,14 @@ "app.editPipelineForm.validation.description": "Prosíme vyplňte popis této pipeliny.", "app.editPipelineForm.validation.emptyName": "Prosíme vyplňte název této pipeliny.", "app.editPipelineForm.validation.versionDiffers": "Někdo jiný změnit nastavení této pipeliny v průběhu vaší editace. Prosíme znovu načtěte tuto stránku a aplikujte své změny znovu.", + "app.editRuntimeConfigForm.addConfigTab": "Add new runtime configuration", + "app.editRuntimeConfigForm.configName": "Name of Configuration:", + "app.editRuntimeConfigForm.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editRuntimeConfigForm.jobConfig": "Job Configuration:", + "app.editRuntimeConfigForm.moreAboutJobConfig": "Read more about job configuration syntax.", + "app.editRuntimeConfigForm.newConfig": "New configuration", + "app.editRuntimeConfigForm.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", + "app.editRuntimeConfigForm.runtimeEnvironment": "Select runtime environment:", "app.editScoreConfigForm.failed": "Saving failed. Please try again later.", "app.editScoreConfigForm.scoreConfig": "Score configuration:", "app.editScoreConfigForm.submit": "Change configuration", @@ -444,6 +500,8 @@ "app.editUserProfile.validation.emptyFirstName": "Jméno nemůže být prázdné.", "app.editUserProfile.validation.emptyLastName": "Příjmení nemůže být prázdné.", "app.editUserProfile.validation.emptyNewPassword": "Nové heslo nemůže být prázdné pokud si měníte heslo.", + "app.editUserProfile.validation.emptyOldPassword": "Old password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.editUserProfile.validation.passwordsDontMatch": "Hesla se neshodují.", "app.editUserProfile.validation.samePasswords": "Změnit Vaše heslo na stejné nedává žádný smysl.", "app.editUserProfile.validation.shortFirstName": "First name must contain at least 2 characters.", @@ -692,6 +750,15 @@ "app.filesTable.title": "Attached files", "app.footer.copyright": "Copyright © 2016-2018 ReCodEx. Všechna práva vyhrazena.", "app.footer.version": "Verze {version}", + "app.forkExerciseButton.confirmation": "Do you really want to fork this exercise?", + "app.forkExerciseButton.failed": "Try forking the exercise again", + "app.forkExerciseButton.fork": "Fork the exercise", + "app.forkExerciseButton.loading": "Forking ...", + "app.forkExerciseButton.success": "Show the forked exercise", + "app.forkExerciseForm.failed": "Saving failed. Please try again later.", + "app.forkExerciseForm.submit": "Fork exercise", + "app.forkExerciseForm.submitting": "Forking ...", + "app.forkExerciseForm.success": "Exercise forked", "app.forkPipelineButton.success": "Show the forked pipeline", "app.forkPipelineForm.failed": "Saving failed. Please try again later.", "app.forkPipelineForm.submit": "Fork pipeline", @@ -730,6 +797,10 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Odebrat cvičícího", "app.groupsName.loading": "Načítání ...", + "app.hardwareGroupFields.memoryLimit": "Memory limit for \"{taskId}\":", + "app.hardwareGroupFields.noReferenceSolutions": "There are no reference solutions' evaluations' for test '{testName}' and its task '{taskId}'.", + "app.hardwareGroupFields.test": "Test:", + "app.hardwareGroupFields.timeLimit": "Time limit for \"{taskId}\":", "app.header.toggleSidebar": "Zobrazit/skrýt boční panel", "app.header.toggleSidebarSize": "Zvětšit/zmenšit boční panel", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -794,6 +865,29 @@ "app.maybePublicIcon.isNotPublic": "Není veřejné", "app.maybePublicIcon.isPublic": "Je veřejné", "app.milisecondsTextField.humanReadable": "Čitelná podoba:", + "app.multiAssignForm.allowSecondDeadline": "Allow second deadline.", + "app.multiAssignForm.canViewLimitRatios": "Visibility of memory and time ratios", + "app.multiAssignForm.chooseFirstDeadlineBeforeSecondDeadline": "You must select the date of the first deadline before selecting the date of the second deadline.", + "app.multiAssignForm.failed": "Saving failed. Please try again later.", + "app.multiAssignForm.firstDeadline": "First deadline:", + "app.multiAssignForm.isBonus": "Assignment is bonus one and points from it are not included in students overall score", + "app.multiAssignForm.maxPointsBeforeFirstDeadline": "Maximum amount of points received when submitted before the deadline:", + "app.multiAssignForm.maxPointsBeforeSecondDeadline": "Maximum amount of points received when submitted before the second deadline:", + "app.multiAssignForm.pointsPercentualThreshold": "Minimum percentage of points which submissions have to gain:", + "app.multiAssignForm.secondDeadline": "Second deadline:", + "app.multiAssignForm.submissionsCountLimit": "Submissions count limit:", + "app.multiAssignForm.submit": "Assign exercise", + "app.multiAssignForm.submitting": "Assigning exercise ...", + "app.multiAssignForm.success": "Exercise was assigned.", + "app.multiAssignForm.validation.emptyDeadline": "Please fill the date and time of the deadline.", + "app.multiAssignForm.validation.emptyGroups": "Please select one or more groups to assign exercise.", + "app.multiAssignForm.validation.maxPointsBeforeFirstDeadline": "Please fill the maximum number of points received when submitted before the deadline with a nonnegative integer.", + "app.multiAssignForm.validation.maxPointsBeforeSecondDeadline": "Please fill the number of maximu points received after the first and before the second deadline with a nonnegative integer or remove the second deadline.", + "app.multiAssignForm.validation.pointsPercentualThresholdBetweenZeroHundred": "Points percentual threshold must be an integer in between 0 and 100.", + "app.multiAssignForm.validation.pointsPercentualThresholdMustBeInteger": "Points percentual threshold must be an integer.", + "app.multiAssignForm.validation.secondDeadline": "Please fill the date and time of the second deadline.", + "app.multiAssignForm.validation.secondDeadlineBeforeFirstDeadline": "Please fill the date and time of the second deadline with a value which is after {firstDeadline, date} {firstDeadline, time, short}.", + "app.multiAssignForm.validation.submissionsCountLimit": "Please fill the submissions count limit field with a positive integer.", "app.notFound.description": "Oops, tohle jste pravděpodobně nehledali.", "app.notFound.text": "Jazyková mutace podle tohoto URL není dostupná.", "app.notFound.title": "Stránka nenalezena", @@ -850,6 +944,12 @@ "app.pipelineEditor.BoxForm.success": "Saved", "app.pipelineEditor.BoxForm.type": "Type:", "app.pipelineEditor.EditBoxForm.title": "Edit the box '{name}'", + "app.pipelineEditor.addBoxForm.add": "Add", + "app.pipelineEditor.addBoxForm.emptyName": "Name cannot be empty.", + "app.pipelineEditor.addBoxForm.name": "Box name", + "app.pipelineEditor.addBoxForm.portsIn": "Inputs", + "app.pipelineEditor.addBoxForm.portsOut": "Outputs", + "app.pipelineEditor.addBoxForm.title": "Add a box", "app.pipelineFilesTable.description": "Supplementary files are files which can be referenced as remote file in pipeline configuration.", "app.pipelineFilesTable.title": "Supplementary files", "app.pipelineVisualEditor.addBoxButton": "Add box", @@ -918,6 +1018,12 @@ "app.referenceSolutionDetail.title.details": "Detail referenčního řešení", "app.referenceSolutionDetail.uploadedAt": "Nahráno:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", + "app.referenceSolutionEvaluation.titlePrefix": "Evaluations for runtime:", + "app.referenceSolutionsEvaluations.description": "Description", + "app.referenceSolutionsEvaluations.evaluatedAt": "Evaluated on", + "app.referenceSolutionsEvaluations.memory": "Memory", + "app.referenceSolutionsEvaluations.time": "Time", + "app.referenceSolutionsEvaluations.title": "Reference solutions' evaluations", "app.registration.description": "Začněte dnes používat ReCodEx", "app.registration.title": "Vytvořte si nový účet v ReCodExu", "app.registrationForm.createAccount": "Vytvořit účet", @@ -938,6 +1044,7 @@ "app.registrationForm.validation.emptyLastName": "Příjmení nemůže být prázdné.", "app.registrationForm.validation.emptyPassword": "Heslo nemůže být prázdné.", "app.registrationForm.validation.passwordDontMatch": "Passwords don't match.", + "app.registrationForm.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.registrationForm.validation.shortFirstName": "First name must contain at least 2 characters.", "app.registrationForm.validation.shortLastName": "Last name must contain at least 2 characters.", "app.removeFromGroup.confirm": "Are you sure you want to remove the user from this group?", diff --git a/src/locales/en.json b/src/locales/en.json index 7387e4096..dbbb68b4e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -153,12 +153,22 @@ "app.confirm.no": "No", "app.confirm.yes": "Yes", "app.createGroup.externalId": "External ID of the group (e. g. ID of the group in the school IS):", + "app.createGroup.groupDescription": "Description:", + "app.createGroup.groupName": "Name:", "app.createGroup.hasThreshold": "Students require cetrain number of points to complete the course", "app.createGroup.isPublic": "Public (everyone can see and join this group)", "app.createGroup.publicStats": "Students can see statistics of each other", "app.createGroup.threshold": "Minimum percent of the total points count needed to complete the course:", + "app.createGroup.validation.emptyDescription": "Group description cannot be empty.", + "app.createGroup.validation.emptyName": "Group name cannot be empty.", + "app.createGroup.validation.nameCollision": "The name \"{name}\" is already used, please choose a different one.", "app.createGroup.validation.thresholdBetweenZeroHundred": "Threshold must be an integer in between 0 and 100.", "app.createGroup.validation.thresholdMustBeInteger": "Threshold must be an integer.", + "app.createGroupForm.createGroup": "Create new group", + "app.createGroupForm.failed": "We are sorry but we weren't able to create a new group.", + "app.createGroupForm.processing": "Group is being created ...", + "app.createGroupForm.success": "Group has been created", + "app.createGroupForm.title": "Create new group", "app.createGroupForm.validation.noLocalizedText": "Please add at least one localized text describing the group.", "app.dashboard.sisGroupsStudent": "SIS Courses - Student", "app.dashboard.sisGroupsStudentExplain": "SIS courses you are enrolled to in particular semesters and which have correspondig groups in ReCodEx.", @@ -175,6 +185,7 @@ "app.editAssignment.deleteAssignmentWarning": "Deleting an assignment will remove all the students submissions and you will have to contact the administrator of ReCodEx if you wanted to restore the assignment in the future.", "app.editAssignment.description": "Change assignment settings and limits", "app.editAssignment.title": "Edit assignment settings", + "app.editAssignmentForm.addLanguage": "Add language variant", "app.editAssignmentForm.allowSecondDeadline": "Allow second deadline.", "app.editAssignmentForm.canViewLimitRatios": "Visibility of memory and time ratios", "app.editAssignmentForm.chooseFirstDeadlineBeforeSecondDeadline": "You must select the date of the first deadline before selecting the date of the second deadline.", @@ -186,9 +197,16 @@ "app.editAssignmentForm.localized.assignment": "Description for the students:", "app.editAssignmentForm.localized.locale": "The language:", "app.editAssignmentForm.localized.name": "Name:", + "app.editAssignmentForm.localized.noLanguage": "There is currently no text in any language for this assignment.", + "app.editAssignmentForm.localized.reallyRemoveQuestion": "Do you really want to delete the assignmenet in this language?", + "app.editAssignmentForm.localized.remove": "Remove this language", "app.editAssignmentForm.maxPointsBeforeFirstDeadline": "Maximum amount of points received when submitted before the deadline:", "app.editAssignmentForm.maxPointsBeforeSecondDeadline": "Maximum amount of points received when submitted before the second deadline:", + "app.editAssignmentForm.moreAboutScoreConfig": "Read more about score configuration syntax.", + "app.editAssignmentForm.name": "Assignment default name:", + "app.editAssignmentForm.newLocale": "New language", "app.editAssignmentForm.pointsPercentualThreshold": "Minimum percentage of points which submissions have to gain:", + "app.editAssignmentForm.scoreConfig": "Score configuration:", "app.editAssignmentForm.secondDeadline": "Second deadline:", "app.editAssignmentForm.submissionsCountLimit": "Submissions count limit:", "app.editAssignmentForm.submit": "Edit settings", @@ -235,6 +253,13 @@ "app.editEnvironmentConfigVariables.stringArrayType": "Array of strings", "app.editEnvironmentConfigVariables.stringType": "String", "app.editEnvironmentConfigVariables.variables": "Variables:", + "app.editEnvironmentLimitsForm.box": "Box", + "app.editEnvironmentLimitsForm.environment.name": "Environment name:", + "app.editEnvironmentLimitsForm.environment.noEnvironment": "There is currently no environment specified for this assignment.", + "app.editEnvironmentLimitsForm.newEnvironment": "New environment", + "app.editEnvironmentLimitsForm.noBoxesForPipeline": "There are no boxes which need to set limits in this pipeline.", + "app.editEnvironmentLimitsForm.noPipelinesForTest": "There are no pipelines for this test to edit.", + "app.editEnvironmentLimitsForm.pipeline": "Pipeline", "app.editEnvironmentLimitsForm.submit": "Save changes to {env}", "app.editEnvironmentLimitsForm.submitting": "Saving changes ...", "app.editEnvironmentLimitsForm.success": "Saved.", @@ -269,10 +294,12 @@ "app.editExerciseConfigEnvironment.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", "app.editExerciseConfigForm.addTest": "Add new test", "app.editExerciseConfigForm.failed": "Saving failed. Please try again later.", + "app.editExerciseConfigForm.fileType": "File", "app.editExerciseConfigForm.pipelines": "Pipelines", "app.editExerciseConfigForm.removeLastTest": "Remove last test", "app.editExerciseConfigForm.smartFill": "Smart Fill", "app.editExerciseConfigForm.smartFill.yesNoQuestion": "Do you really wish to overwrite configuration of all subsequent tests using the first test as a template? Files will be paired to individual test configurations by a heuristics based on matching name substrings.", + "app.editExerciseConfigForm.stringType": "String", "app.editExerciseConfigForm.submit": "Change configuration", "app.editExerciseConfigForm.submitting": "Saving configuration ...", "app.editExerciseConfigForm.success": "Configuration was changed.", @@ -280,6 +307,7 @@ "app.editExerciseConfigForm.validation.duplicatePipeline": "Please select a different pipeline.", "app.editExerciseConfigForm.validation.noEnvironments": "Please add at least one environment config for the exercise.", "app.editExerciseConfigForm.variables": "Variables", + "app.editExerciseForm.description": "Description for supervisors:", "app.editExerciseForm.difficulty": "Difficulty", "app.editExerciseForm.easy": "Easy", "app.editExerciseForm.failed": "Saving failed. Please try again later.", @@ -288,6 +316,7 @@ "app.editExerciseForm.isLocked": "Exercise is locked (visible, but cannot be assigned to any group).", "app.editExerciseForm.isPublic": "Exercise is public and can be assigned to students by their supervisors.", "app.editExerciseForm.medium": "Medium", + "app.editExerciseForm.name": "Exercise name:", "app.editExerciseForm.submit": "Edit settings", "app.editExerciseForm.submitting": "Saving changes ...", "app.editExerciseForm.success": "Settings were saved.", @@ -302,6 +331,24 @@ "app.editExerciseForm.validation.noLocalizedText": "Please add at least one localized text describing the exercise.", "app.editExerciseForm.validation.sameLocalizedTexts": "There are more language variants with the same locale. Please make sure locales are unique.", "app.editExerciseForm.validation.versionDiffers": "Somebody has changed the exercise while you have been editing it. Please reload the page and apply your changes once more.", + "app.editExerciseLimitsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseLimitsForm.submit": "Change limits", + "app.editExerciseLimitsForm.submitting": "Saving limits ...", + "app.editExerciseLimitsForm.success": "Limits were saved.", + "app.editExerciseLimitsForm.validation.envName": "Please fill environment name.", + "app.editExerciseLimitsForm.validation.memoryIsNotNumer": "Memory limit must be an integer.", + "app.editExerciseLimitsForm.validation.memoryLimit": "Memory limit must be a positive integer.", + "app.editExerciseLimitsForm.validation.timeIsNotNumer": "Time limit must be a real number.", + "app.editExerciseLimitsForm.validation.timeLimit": "Time limit must be a positive real number.", + "app.editExerciseLimitsForm.validation.useDotDecimalSeparator": "Please use a dot as a decimal separator instead of the comma.", + "app.editExerciseRuntimeConfigsForm.failed": "Saving failed. Please try again later.", + "app.editExerciseRuntimeConfigsForm.submit": "Change runtime configurations", + "app.editExerciseRuntimeConfigsForm.submitting": "Saving runtime configurations ...", + "app.editExerciseRuntimeConfigsForm.success": "Runtime configurations were saved.", + "app.editExerciseRuntimeConfigsForm.validation.empty": "Please fill the runtime environment information.", + "app.editExerciseRuntimeConfigsForm.validation.jobConfig": "Please fill the job configuration of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.name": "Please fill the display name of the runtime environment.", + "app.editExerciseRuntimeConfigsForm.validation.runtimeEnvironmentId": "Please select a runtime environment.", "app.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", @@ -343,6 +390,7 @@ "app.editGroupForm.set": "Edit group", "app.editGroupForm.success": "Group settings was saved.", "app.editGroupForm.successNew": "Create group", + "app.editGroupForm.title": "Edit group", "app.editGroupForm.titleEdit": "Edit group", "app.editGroupForm.titleNew": "Create new group", "app.editGroupForm.validation.emptyName": "Please fill the name of the group.", @@ -385,6 +433,14 @@ "app.editPipelineForm.validation.description": "Please fill the description of the pipeline.", "app.editPipelineForm.validation.emptyName": "Please fill the name of the pipeline.", "app.editPipelineForm.validation.versionDiffers": "Somebody has changed the pipeline while you have been editing it. Please reload the page and apply your changes once more.", + "app.editRuntimeConfigForm.addConfigTab": "Add new runtime configuration", + "app.editRuntimeConfigForm.configName": "Name of Configuration:", + "app.editRuntimeConfigForm.emptyConfigTabs": "There is currently no runtime configuration.", + "app.editRuntimeConfigForm.jobConfig": "Job Configuration:", + "app.editRuntimeConfigForm.moreAboutJobConfig": "Read more about job configuration syntax.", + "app.editRuntimeConfigForm.newConfig": "New configuration", + "app.editRuntimeConfigForm.reallyRemoveQuestion": "Do you really want to delete this runtime configuration?", + "app.editRuntimeConfigForm.runtimeEnvironment": "Select runtime environment:", "app.editScoreConfigForm.failed": "Saving failed. Please try again later.", "app.editScoreConfigForm.scoreConfig": "Score configuration:", "app.editScoreConfigForm.submit": "Change configuration", @@ -444,6 +500,8 @@ "app.editUserProfile.validation.emptyFirstName": "First name cannot be empty.", "app.editUserProfile.validation.emptyLastName": "Last name cannot be empty.", "app.editUserProfile.validation.emptyNewPassword": "New password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.emptyOldPassword": "Old password cannot be empty if you want to change your password.", + "app.editUserProfile.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.editUserProfile.validation.passwordsDontMatch": "Passwords don't match.", "app.editUserProfile.validation.samePasswords": "Changing your password to the same password does not make any sense.", "app.editUserProfile.validation.shortFirstName": "First name must contain at least 2 characters.", @@ -515,7 +573,7 @@ "app.exercise.exercisePipelines": "Exercise Pipelines", "app.exercise.forked": "Forked from:", "app.exercise.groups": "Groups:", - "app.exercise.groupsBox": "Groups", + "app.exercise.groupsBox": "Assign to groups", "app.exercise.isBroken": "Exercise configuration is incorrect and needs fixing", "app.exercise.isLocked": "Is locked:", "app.exercise.isPublic": "Is public:", @@ -692,6 +750,15 @@ "app.filesTable.title": "Attached files", "app.footer.copyright": "Copyright © 2016-2018 ReCodEx. All rights reserved.", "app.footer.version": "Version {version}", + "app.forkExerciseButton.confirmation": "Do you really want to fork this exercise?", + "app.forkExerciseButton.failed": "Try forking the exercise again", + "app.forkExerciseButton.fork": "Fork the exercise", + "app.forkExerciseButton.loading": "Forking ...", + "app.forkExerciseButton.success": "Show the forked exercise", + "app.forkExerciseForm.failed": "Saving failed. Please try again later.", + "app.forkExerciseForm.submit": "Fork exercise", + "app.forkExerciseForm.submitting": "Forking ...", + "app.forkExerciseForm.success": "Exercise forked", "app.forkPipelineButton.success": "Show the forked pipeline", "app.forkPipelineForm.failed": "Saving failed. Please try again later.", "app.forkPipelineForm.submit": "Fork pipeline", @@ -730,6 +797,10 @@ "app.groups.removeGroupAdminButton": "Remove group admin", "app.groups.removeSupervisorButton": "Remove supervisor", "app.groupsName.loading": "Loading ...", + "app.hardwareGroupFields.memoryLimit": "Memory limit for \"{taskId}\":", + "app.hardwareGroupFields.noReferenceSolutions": "There are no reference solutions' evaluations' for test '{testName}' and its task '{taskId}'.", + "app.hardwareGroupFields.test": "Test:", + "app.hardwareGroupFields.timeLimit": "Time limit for \"{taskId}\":", "app.header.toggleSidebar": "Show/hide sidebar", "app.header.toggleSidebarSize": "Expand/minimize sidebar", "app.headerNotification.copiedToClippboard": "Copied to clippboard.", @@ -794,6 +865,29 @@ "app.maybePublicIcon.isNotPublic": "Is not public", "app.maybePublicIcon.isPublic": "Is public", "app.milisecondsTextField.humanReadable": "Human readable variant:", + "app.multiAssignForm.allowSecondDeadline": "Allow second deadline.", + "app.multiAssignForm.canViewLimitRatios": "Visibility of memory and time ratios", + "app.multiAssignForm.chooseFirstDeadlineBeforeSecondDeadline": "You must select the date of the first deadline before selecting the date of the second deadline.", + "app.multiAssignForm.failed": "Saving failed. Please try again later.", + "app.multiAssignForm.firstDeadline": "First deadline:", + "app.multiAssignForm.isBonus": "Assignment is bonus one and points from it are not included in students overall score", + "app.multiAssignForm.maxPointsBeforeFirstDeadline": "Maximum amount of points received when submitted before the deadline:", + "app.multiAssignForm.maxPointsBeforeSecondDeadline": "Maximum amount of points received when submitted before the second deadline:", + "app.multiAssignForm.pointsPercentualThreshold": "Minimum percentage of points which submissions have to gain:", + "app.multiAssignForm.secondDeadline": "Second deadline:", + "app.multiAssignForm.submissionsCountLimit": "Submissions count limit:", + "app.multiAssignForm.submit": "Assign exercise", + "app.multiAssignForm.submitting": "Assigning exercise ...", + "app.multiAssignForm.success": "Exercise was assigned.", + "app.multiAssignForm.validation.emptyDeadline": "Please fill the date and time of the deadline.", + "app.multiAssignForm.validation.emptyGroups": "Please select one or more groups to assign exercise.", + "app.multiAssignForm.validation.maxPointsBeforeFirstDeadline": "Please fill the maximum number of points received when submitted before the deadline with a nonnegative integer.", + "app.multiAssignForm.validation.maxPointsBeforeSecondDeadline": "Please fill the number of maximu points received after the first and before the second deadline with a nonnegative integer or remove the second deadline.", + "app.multiAssignForm.validation.pointsPercentualThresholdBetweenZeroHundred": "Points percentual threshold must be an integer in between 0 and 100.", + "app.multiAssignForm.validation.pointsPercentualThresholdMustBeInteger": "Points percentual threshold must be an integer.", + "app.multiAssignForm.validation.secondDeadline": "Please fill the date and time of the second deadline.", + "app.multiAssignForm.validation.secondDeadlineBeforeFirstDeadline": "Please fill the date and time of the second deadline with a value which is after {firstDeadline, date} {firstDeadline, time, short}.", + "app.multiAssignForm.validation.submissionsCountLimit": "Please fill the submissions count limit field with a positive integer.", "app.notFound.description": "Oops, this is probably not what you were looking for.", "app.notFound.text": "The URL is not a word of the language this website accepts.", "app.notFound.title": "Page not found", @@ -850,6 +944,12 @@ "app.pipelineEditor.BoxForm.success": "Saved", "app.pipelineEditor.BoxForm.type": "Type:", "app.pipelineEditor.EditBoxForm.title": "Edit the box '{name}'", + "app.pipelineEditor.addBoxForm.add": "Add", + "app.pipelineEditor.addBoxForm.emptyName": "Name cannot be empty.", + "app.pipelineEditor.addBoxForm.name": "Box name", + "app.pipelineEditor.addBoxForm.portsIn": "Inputs", + "app.pipelineEditor.addBoxForm.portsOut": "Outputs", + "app.pipelineEditor.addBoxForm.title": "Add a box", "app.pipelineFilesTable.description": "Supplementary files are files which can be referenced as remote file in pipeline configuration.", "app.pipelineFilesTable.title": "Supplementary files", "app.pipelineVisualEditor.addBoxButton": "Add box", @@ -918,6 +1018,12 @@ "app.referenceSolutionDetail.title.details": "Reference solution detail", "app.referenceSolutionDetail.uploadedAt": "Uploaded at:", "app.referenceSolutionEvaluation.title": "Evaluations of reference solution", + "app.referenceSolutionEvaluation.titlePrefix": "Evaluations for runtime:", + "app.referenceSolutionsEvaluations.description": "Description", + "app.referenceSolutionsEvaluations.evaluatedAt": "Evaluated on", + "app.referenceSolutionsEvaluations.memory": "Memory", + "app.referenceSolutionsEvaluations.time": "Time", + "app.referenceSolutionsEvaluations.title": "Reference solutions' evaluations", "app.registration.description": "Start using ReCodEx today", "app.registration.title": "Create a new ReCodEx account", "app.registrationForm.createAccount": "Create account", @@ -938,6 +1044,7 @@ "app.registrationForm.validation.emptyLastName": "Last name cannot be empty.", "app.registrationForm.validation.emptyPassword": "Password cannot be empty.", "app.registrationForm.validation.passwordDontMatch": "Passwords don't match.", + "app.registrationForm.validation.passwordTooWeak": "The password you chose is too weak, please choose a different one.", "app.registrationForm.validation.shortFirstName": "First name must contain at least 2 characters.", "app.registrationForm.validation.shortLastName": "Last name must contain at least 2 characters.", "app.removeFromGroup.confirm": "Are you sure you want to remove the user from this group?", diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index 5901e44b5..1cd739cd1 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -12,6 +12,8 @@ import { import { Row, Col, ButtonGroup } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import Icon from 'react-fontawesome'; +import moment from 'moment'; +import { formValueSelector } from 'redux-form'; import Button from '../../components/widgets/FlatButton'; import Page from '../../components/layout/Page'; @@ -19,7 +21,6 @@ import ExerciseDetail from '../../components/Exercises/ExerciseDetail'; import LocalizedTexts from '../../components/helpers/LocalizedTexts'; import ResourceRenderer from '../../components/helpers/ResourceRenderer'; import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames'; -import GroupsList from '../../components/Groups/GroupsList'; import ReferenceSolutionsList from '../../components/Exercises/ReferenceSolutionsList'; import SubmitSolutionContainer from '../../containers/SubmitSolutionContainer'; import Box from '../../components/widgets/Box'; @@ -32,7 +33,6 @@ import { import Confirm from '../../components/forms/Confirm'; import PipelinesSimpleList from '../../components/Pipelines/PipelinesSimpleList'; import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; -import AssignExerciseButton from '../../components/buttons/AssignExerciseButton'; import { isSubmitting } from '../../redux/selectors/submission'; import { @@ -45,7 +45,10 @@ import { } from '../../redux/modules/referenceSolutions'; import { createReferenceSolution, init } from '../../redux/modules/submission'; import { fetchHardwareGroups } from '../../redux/modules/hwGroups'; -import { create as assignExercise } from '../../redux/modules/assignments'; +import { + create as assignExercise, + editAssignment +} from '../../redux/modules/assignments'; import { exerciseSelector } from '../../redux/selectors/exercises'; import { referenceSolutionsSelector } from '../../redux/selectors/referenceSolutions'; import { @@ -65,7 +68,6 @@ import { import { loggedInUserIdSelector } from '../../redux/selectors/auth'; import { - supervisorOfSelector, groupsSelector, groupDataAccessorSelector } from '../../redux/selectors/groups'; @@ -73,11 +75,12 @@ import { import withLinks from '../../hoc/withLinks'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer'; import { fetchUser } from '../../redux/modules/users'; +import MultiAssignForm from '../../components/forms/MultiAssignForm'; const messages = defineMessages({ groupsBox: { id: 'app.exercise.groupsBox', - defaultMessage: 'Groups' + defaultMessage: 'Assign to groups' }, referenceSolutionsBox: { id: 'app.exercise.referenceSolutionsBox', @@ -125,13 +128,35 @@ class Exercise extends Component { this.setState({ forkId: Math.random().toString() }); } - assignExercise = groupId => { - const { assignExercise, push } = this.props; - const { links: { ASSIGNMENT_EDIT_URI_FACTORY } } = this.context; + assignExercise = formData => { + const { assignExercise, editAssignment } = this.props; - assignExercise(groupId).then(({ value: assigment }) => - push(ASSIGNMENT_EDIT_URI_FACTORY(assigment.id)) - ); + const groups = + formData && formData.groups + ? Object.keys(formData.groups).filter(key => formData.groups[key]) + : []; + + let actions = []; + + for (const groupId of groups) { + assignExercise(groupId).then(({ value: assigment }) => { + let assignmentData = Object.assign({}, assigment, formData, { + firstDeadline: moment(formData.firstDeadline).unix(), + secondDeadline: moment(formData.secondDeadline).unix(), + submissionsCountLimit: Number(formData.submissionsCountLimit), + isPublic: true + }); + if (!assignmentData.allowSecondDeadline) { + delete assignmentData.secondDeadline; + delete assignmentData.maxPointsBeforeSecondDeadline; + } + delete assignmentData.groups; + + return editAssignment(assigment.id, assignmentData); + }); + } + + return Promise.all(actions); }; createExercisePipeline = () => { @@ -150,7 +175,6 @@ class Exercise extends Component { userId, exercise, submitting, - supervisedGroups, canEditExercise, referenceSolutions, intl: { formatMessage, locale }, @@ -161,7 +185,9 @@ class Exercise extends Component { groups, groupsAccessor, forkExercise, - isSuperAdmin + isSuperAdmin, + firstDeadline, + allowSecondDeadline } = this.props; const { forkId } = this.state; @@ -277,32 +303,28 @@ class Exercise extends Component { } {!exercise.isBroken && + !exercise.isLocked &&

} - noPadding + unlimitedHeight > - - {() => - - - this.assignExercise(groupId)} - />} + + {visibleGroups => + }
} @@ -497,6 +519,7 @@ Exercise.propTypes = { }).isRequired, loadAsync: PropTypes.func.isRequired, assignExercise: PropTypes.func.isRequired, + editAssignment: PropTypes.func.isRequired, push: PropTypes.func.isRequired, exercise: ImmutablePropTypes.map, supervisedGroups: PropTypes.object, @@ -512,9 +535,13 @@ Exercise.propTypes = { forkExercise: PropTypes.func.isRequired, groups: ImmutablePropTypes.map, isSuperAdmin: PropTypes.bool, - groupsAccessor: PropTypes.func.isRequired + groupsAccessor: PropTypes.func.isRequired, + firstDeadline: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), + allowSecondDeadline: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) }; +const editMultiAssignFormSelector = formValueSelector('multiAssign'); + export default withLinks( injectIntl( connect( @@ -525,14 +552,18 @@ export default withLinks( 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), groupsAccessor: groupDataAccessorSelector(state), - isSuperAdmin: isLoggedAsSuperAdmin(state) + isSuperAdmin: isLoggedAsSuperAdmin(state), + firstDeadline: editMultiAssignFormSelector(state, 'firstDeadline'), + allowSecondDeadline: editMultiAssignFormSelector( + state, + 'allowSecondDeadline' + ) }; }, (dispatch, { params: { exerciseId } }) => ({ @@ -540,6 +571,7 @@ export default withLinks( Exercise.loadAsync({ exerciseId }, dispatch, userId), assignExercise: groupId => dispatch(assignExercise(groupId, exerciseId)), + editAssignment: (id, body) => dispatch(editAssignment(id, body)), push: url => dispatch(push(url)), initCreateReferenceSolution: userId => dispatch(init(userId, exerciseId)), From 5b2adef09c4ccde5faeeb5e0a924c230a9f632e7 Mon Sep 17 00:00:00 2001 From: Petr Stefan Date: Sun, 25 Feb 2018 00:01:51 +0100 Subject: [PATCH 4/4] Fix merge --- src/pages/Exercise/Exercise.js | 42 ++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index ce717f162..586583aea 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -13,6 +13,7 @@ import { Row, Col } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import Icon from 'react-fontawesome'; import { formValueSelector } from 'redux-form'; +import moment from 'moment'; import SupplementaryFilesTableContainer from '../../containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer'; import Button from '../../components/widgets/FlatButton'; @@ -46,7 +47,10 @@ import { } from '../../redux/modules/referenceSolutions'; import { createReferenceSolution, init } from '../../redux/modules/submission'; import { fetchHardwareGroups } from '../../redux/modules/hwGroups'; -import { create as assignExercise } from '../../redux/modules/assignments'; +import { + create as assignExercise, + editAssignment +} from '../../redux/modules/assignments'; import { exerciseSelector } from '../../redux/selectors/exercises'; import { referenceSolutionsSelector } from '../../redux/selectors/referenceSolutions'; import { @@ -126,13 +130,35 @@ class Exercise extends Component { this.setState({ forkId: Math.random().toString() }); } - assignExercise = groupId => { - const { assignExercise, push } = this.props; - const { links: { ASSIGNMENT_EDIT_URI_FACTORY } } = this.context; + assignExercise = formData => { + const { assignExercise, editAssignment } = this.props; - assignExercise(groupId).then(({ value: assigment }) => - push(ASSIGNMENT_EDIT_URI_FACTORY(assigment.id)) - ); + const groups = + formData && formData.groups + ? Object.keys(formData.groups).filter(key => formData.groups[key]) + : []; + + let actions = []; + + for (const groupId of groups) { + assignExercise(groupId).then(({ value: assigment }) => { + let assignmentData = Object.assign({}, assigment, formData, { + firstDeadline: moment(formData.firstDeadline).unix(), + secondDeadline: moment(formData.secondDeadline).unix(), + submissionsCountLimit: Number(formData.submissionsCountLimit), + isPublic: true + }); + if (!assignmentData.allowSecondDeadline) { + delete assignmentData.secondDeadline; + delete assignmentData.maxPointsBeforeSecondDeadline; + } + delete assignmentData.groups; + + return editAssignment(assigment.id, assignmentData); + }); + } + + return Promise.all(actions); }; createExercisePipeline = () => { @@ -464,6 +490,7 @@ Exercise.propTypes = { }).isRequired, loadAsync: PropTypes.func.isRequired, assignExercise: PropTypes.func.isRequired, + editAssignment: PropTypes.func.isRequired, push: PropTypes.func.isRequired, exercise: ImmutablePropTypes.map, supervisedGroups: PropTypes.object, @@ -512,6 +539,7 @@ export default withLinks( (dispatch, { params: { exerciseId } }) => ({ loadAsync: userId => Exercise.loadAsync({ exerciseId }, dispatch, userId), assignExercise: groupId => dispatch(assignExercise(groupId, exerciseId)), + editAssignment: (id, body) => dispatch(editAssignment(id, body)), push: url => dispatch(push(url)), initCreateReferenceSolution: userId => dispatch(init(userId, exerciseId)), createExercisePipeline: () =>