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/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 7c7dcef5c..fb627bccd 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 '../../../helpers/withLinks'; @@ -41,6 +41,7 @@ class ForkExerciseForm extends Component { submitSucceeded, 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/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 abf91c578..74fbf8b9b 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -154,12 +154,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.", @@ -168,10 +178,14 @@ "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": "Delete", + "app.deleteButton.deleted": "Deleted.", + "app.deleteButton.deleting": "Deleting failed", "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ů", "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.", @@ -183,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í", @@ -232,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.", @@ -266,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.", @@ -277,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.", @@ -284,6 +315,7 @@ "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.name": "Exercise name:", "app.editExerciseForm.submit": "Upravit nastavení", "app.editExerciseForm.submitting": "Ukládání změn ...", "app.editExerciseForm.success": "Nastavení bylo uloženo.", @@ -304,6 +336,24 @@ "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.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.editExerciseSimpleConfigForm.reset": "Obnovit původní", "app.editExerciseSimpleConfigForm.submit": "Uložit konfiguraci", @@ -344,6 +394,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.", @@ -377,7 +428,6 @@ "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.", @@ -402,6 +452,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", @@ -461,6 +519,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.", @@ -706,6 +766,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", @@ -744,6 +813,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.hardwareGroupMetadata.cpuTimeOverlay": "Precise (CPU) time limit constraints", "app.hardwareGroupMetadata.description": "Internal Description:", "app.hardwareGroupMetadata.id": "Internal Identifier:", @@ -818,6 +891,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", @@ -874,6 +970,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", @@ -942,6 +1044,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", @@ -962,6 +1070,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 2127990bd..e188c64c1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -154,12 +154,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.", @@ -168,10 +178,14 @@ "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 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", "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.", @@ -183,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", @@ -232,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.", @@ -266,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.", @@ -277,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.", @@ -284,6 +315,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.", @@ -304,6 +336,24 @@ "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.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.editExerciseSimpleConfigForm.reset": "Reset", "app.editExerciseSimpleConfigForm.submit": "Save Configuration", @@ -344,6 +394,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.", @@ -377,7 +428,6 @@ "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.", @@ -402,6 +452,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", @@ -461,6 +519,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.", @@ -529,7 +589,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:", @@ -706,6 +766,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", @@ -744,6 +813,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.hardwareGroupMetadata.cpuTimeOverlay": "Precise (CPU) time limit constraints", "app.hardwareGroupMetadata.description": "Internal Description:", "app.hardwareGroupMetadata.id": "Internal Identifier:", @@ -818,6 +891,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", @@ -874,6 +970,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", @@ -942,6 +1044,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", @@ -962,6 +1070,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/EditGroup/EditGroup.js b/src/pages/EditGroup/EditGroup.js index 414e6f0b0..f71f7fa10 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 '../../helpers/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 } }) => ({ diff --git a/src/pages/Exercise/Exercise.js b/src/pages/Exercise/Exercise.js index d568f6aba..586583aea 100644 --- a/src/pages/Exercise/Exercise.js +++ b/src/pages/Exercise/Exercise.js @@ -12,6 +12,8 @@ import { 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'; @@ -20,7 +22,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'; @@ -33,7 +34,7 @@ import { import Confirm from '../../components/forms/Confirm'; import PipelinesSimpleList from '../../components/Pipelines/PipelinesSimpleList'; import ExerciseButtons from '../../components/Exercises/ExerciseButtons'; -import AssignExerciseButton from '../../components/buttons/AssignExerciseButton'; +import ForkExerciseForm from '../../components/forms/ForkExerciseForm'; import { isSubmitting } from '../../redux/selectors/submission'; import { @@ -46,30 +47,42 @@ 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 { canLoggedUserEditExercise } from '../../redux/selectors/users'; +import { + canLoggedUserEditExercise, + 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 '../../helpers/withLinks'; +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', @@ -90,7 +103,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() { @@ -109,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 = () => { @@ -134,19 +177,22 @@ class Exercise extends Component { userId, exercise, submitting, - supervisedGroups, canEditExercise, referenceSolutions, intl: { formatMessage, locale }, initCreateReferenceSolution, exercisePipelines, deleteReferenceSolution, - push - // groups, - // forkExercise + push, + groups, + groupsAccessor, + forkExercise, + isSuperAdmin, + firstDeadline, + allowSecondDeadline } = this.props; - // const { forkId } = this.state; + const { forkId } = this.state; const { links: { @@ -206,10 +252,20 @@ class Exercise extends Component { } - - {canEditExercise && - } - + {canEditExercise && + + +

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

+ } @@ -218,32 +274,28 @@ class Exercise extends Component { } {!exercise.isBroken && + !exercise.isLocked &&

} - noPadding + unlimitedHeight > - - {() => - - - this.assignExercise(groupId)} - />} + + {visibleGroups => + }
} @@ -438,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, @@ -451,9 +504,15 @@ 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, + firstDeadline: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), + allowSecondDeadline: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) }; +const editMultiAssignFormSelector = formValueSelector('multiAssign'); + export default withLinks( connect( (state, { params: { exerciseId } }) => { @@ -467,12 +526,20 @@ export default withLinks( canEditExercise: canLoggedUserEditExercise(exerciseId)(state), referenceSolutions: referenceSolutionsSelector(exerciseId)(state), exercisePipelines: exercisePipelinesSelector(exerciseId)(state), - groups: groupsSelector(state) + groups: groupsSelector(state), + groupsAccessor: groupDataAccessorSelector(state), + isSuperAdmin: isLoggedAsSuperAdmin(state), + firstDeadline: editMultiAssignFormSelector(state, 'firstDeadline'), + allowSecondDeadline: editMultiAssignFormSelector( + state, + 'allowSecondDeadline' + ) }; }, (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: () => 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({