Skip to content

Commit

Permalink
Implementing button for (un)setting the group exam flag.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Feb 18, 2024
1 parent 966a04c commit 4f096a9
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 87 deletions.
25 changes: 25 additions & 0 deletions src/components/buttons/ExamGroupButton/ExamGroupButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { GroupIcon, LoadingIcon } from '../../icons';
import Button from '../../widgets/TheButton';

const ExamGroupButton = ({ exam, pending, disabled = false, setExamFlag, ...props }) => (
<Button {...props} variant={disabled ? 'secondary' : 'warning'} onClick={setExamFlag} disabled={pending || disabled}>
{pending ? <LoadingIcon gapRight /> : <GroupIcon exam={!exam} gapRight />}
{exam ? (
<FormattedMessage id="app.groupTypeButton.regular" defaultMessage="Regular" />
) : (
<FormattedMessage id="app.groupTypeButton.exam" defaultMessage="Exam" />
)}
</Button>
);

ExamGroupButton.propTypes = {
exam: PropTypes.bool.isRequired,
pending: PropTypes.bool.isRequired,
disabled: PropTypes.bool,
setExamFlag: PropTypes.func.isRequired,
};

export default ExamGroupButton;
2 changes: 2 additions & 0 deletions src/components/buttons/ExamGroupButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ExamGroupButton from './ExamGroupButton';
export default ExamGroupButton;
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ import { GroupIcon, LoadingIcon } from '../../icons';
import Button from '../../widgets/TheButton';

const OrganizationalGroupButton = ({ organizational, pending, disabled = false, setOrganizational, ...props }) => (
<div className="text-center">
<strong>
<FormattedMessage id="app.organizationalGroupButton.label" defaultMessage="Change type to" />:
</strong>
<br />
<Button
{...props}
variant={disabled ? 'secondary' : 'info'}
onClick={setOrganizational(!organizational)}
disabled={pending || disabled}>
{pending ? <LoadingIcon gapRight /> : <GroupIcon organizational={!organizational} gapRight />}
{organizational ? (
<FormattedMessage id="app.organizationalGroupButton.unset" defaultMessage="Regular" />
) : (
<FormattedMessage id="app.organizationalGroupButton.set" defaultMessage="Organizational" />
)}
</Button>
</div>
<Button
{...props}
variant={disabled ? 'secondary' : 'info'}
onClick={setOrganizational}
disabled={pending || disabled}>
{pending ? <LoadingIcon gapRight /> : <GroupIcon organizational={!organizational} gapRight />}
{organizational ? (
<FormattedMessage id="app.groupTypeButton.regular" defaultMessage="Regular" />
) : (
<FormattedMessage id="app.groupTypeButton.organizational" defaultMessage="Organizational" />
)}
</Button>
);

OrganizationalGroupButton.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ArchiveGroupButtonContainer.propTypes = {

const mapStateToProps = (state, { id }) => ({
group: groupSelector(state, id),
pending: groupArchivedPendingChange(id)(state),
pending: groupArchivedPendingChange(state, id),
});

const mapDispatchToProps = (dispatch, { id, onChange = identity }) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import { defaultMemoize } from 'reselect';

import ExamGroupButton from '../../components/buttons/ExamGroupButton';
import { setExamFlag } from '../../redux/modules/groups';
import { groupSelector, groupTypePendingChange } from '../../redux/selectors/groups';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
import { getErrorMessage } from '../../locales/apiErrorMessages';
import { addNotification } from '../../redux/modules/notifications';

const setExamFlagHandlingErrors = defaultMemoize(
(exam, setExamFlag, addNotification, formatMessage) => () =>
setExamFlag(!exam).catch(err => {
addNotification(getErrorMessage(formatMessage)(err), false);
})
);

const ExamGroupButtonContainer = ({
group,
pending,
setExamFlag,
addNotification,
intl: { formatMessage },
...props
}) => (
<ResourceRenderer resource={group}>
{({ exam, organizational, childGroups, permissionHints }) => (
<ExamGroupButton
exam={exam}
pending={pending}
setExamFlag={setExamFlagHandlingErrors(exam, setExamFlag, addNotification, formatMessage)}
disabled={!permissionHints.setExamFlag || organizational || childGroups.length > 0}
{...props}
/>
)}
</ResourceRenderer>
);

ExamGroupButtonContainer.propTypes = {
id: PropTypes.string.isRequired,
group: ImmutablePropTypes.map,
pending: PropTypes.bool.isRequired,
setExamFlag: PropTypes.func.isRequired,
addNotification: PropTypes.func.isRequired,
intl: PropTypes.object,
};

const mapStateToProps = (state, { id }) => ({
group: groupSelector(state, id),
pending: groupTypePendingChange(state, id),
});

const mapDispatchToProps = (dispatch, { id }) => ({
setExamFlag: value => dispatch(setExamFlag(id, value)),
addNotification: (...args) => dispatch(addNotification(...args)),
});

export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ExamGroupButtonContainer));
2 changes: 2 additions & 0 deletions src/containers/ExamGroupButtonContainer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ExamGroupButtonContainer from './ExamGroupButtonContainer';
export default ExamGroupButtonContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,43 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import { defaultMemoize } from 'reselect';

import OrganizationalGroupButton from '../../components/buttons/OrganizationalGroupButton';
import { setOrganizational } from '../../redux/modules/groups';
import { groupSelector, groupOrganizationalPendingChange } from '../../redux/selectors/groups';
import { groupSelector, groupTypePendingChange } from '../../redux/selectors/groups';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
import { getErrorMessage } from '../../locales/apiErrorMessages';
import { addNotification } from '../../redux/modules/notifications';

const OrganizationalGroupButtonContainer = ({ group, pending, setOrganizational, ...props }) => (
const setOrganizationalHandlingErrors = defaultMemoize(
(organizational, setOrganizational, addNotification, formatMessage) => () =>
setOrganizational(!organizational).catch(err => {
addNotification(getErrorMessage(formatMessage)(err), false);
})
);

const OrganizationalGroupButtonContainer = ({
group,
pending,
setOrganizational,
addNotification,
intl: { formatMessage },
...props
}) => (
<ResourceRenderer resource={group}>
{({ organizational, privateData: { students, assignments }, permissionHints }) => (
{({ exam, organizational, privateData: { students, assignments }, permissionHints }) => (
<OrganizationalGroupButton
organizational={organizational}
pending={pending}
setOrganizational={setOrganizational}
disabled={!permissionHints.update || students.length > 0 || assignments.length > 0}
setOrganizational={setOrganizationalHandlingErrors(
organizational,
setOrganizational,
addNotification,
formatMessage
)}
disabled={!permissionHints.setOrganizational || exam || students.length > 0 || assignments.length > 0}
{...props}
/>
)}
Expand All @@ -27,18 +50,18 @@ OrganizationalGroupButtonContainer.propTypes = {
group: ImmutablePropTypes.map,
pending: PropTypes.bool.isRequired,
setOrganizational: PropTypes.func.isRequired,
addNotification: PropTypes.func.isRequired,
intl: PropTypes.object,
};

const mapStateToProps = (state, { id }) => ({
group: groupSelector(state, id),
pending: groupOrganizationalPendingChange(id)(state),
pending: groupTypePendingChange(state, id),
});

const mapDispatchToProps = (dispatch, { id }) => ({
setOrganizational: organizational => () => dispatch(setOrganizational(id, organizational)),
setOrganizational: organizational => dispatch(setOrganizational(id, organizational)),
addNotification: (...args) => dispatch(addNotification(...args)),
});

export default connect(
mapStateToProps,
mapDispatchToProps
)(OrganizationalGroupButtonContainer);
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(OrganizationalGroupButtonContainer));
33 changes: 30 additions & 3 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,11 @@
"app.editGroup.cannotDeleteGroupWithSubgroups": "Skupinu s podskupinami není možné smazat přímo.",
"app.editGroup.cannotDeleteRootGroup": "Toto je primární skupina a jako taková nemůže být smazána.",
"app.editGroup.changeGroupType": "Změnit typ skupiny",
"app.editGroup.currentType": "Aktuální typ",
"app.editGroup.deleteGroup": "Smazat skupinu",
"app.editGroup.deleteGroupWarning": "Smazání skupiny způsobí, že všechny navázané entity (zadané úlohy, odevzdaná řešení, ...) nebudou přístupné.",
"app.editGroup.examButton": "Vytvořit zkoušku",
"app.editGroup.examExplain": "Zkouškové skupiny fungují uvnitř stejně jako běžné skupiny. Přiznak, že je skupina zkoušková, slouží především jako indikátor pro uživatele. Může také ovlivňovat, jak je skupina zobrazena ve výpisech, nebo kdy dojde k její archivaci. Tento příznak je zcela nezávislý na Zkouškových termínech, které lze zakládat na stránce téhož jména.",
"app.editGroup.organizationalExplain": "Běžné skupiny jsou platformou spojující studenty a zadané úlohy. Organizační skupiny slouží pouze k vytváření hierarchie, takže v nich nesmí být přihlášení studenti a nesmí obsahovat zadané úlohy.",
"app.editGroup.relocateGroup": "Přemístit skupinu",
"app.editGroup.title": "Změnit nastavení skupiny",
Expand Down Expand Up @@ -713,6 +716,18 @@
"app.evaluationTable.evaluationIsDebug": "Vyhodnoceno v ladícím módu (kompletní logy a výpisy)",
"app.evaluationTable.notAvailable": "Vyhodnocení není dostupné",
"app.evaluationTable.score": "Skóre:",
"app.examForm.beginImmediately": "Zahájit okamžitě",
"app.examForm.end": "Konec:",
"app.examForm.endRelative": "Nastavit délku (místo času konce)",
"app.examForm.endRelativeExplanation": "Konec zkoušky je možné nastavit jako explicitní datum a čas (výchozí) nebo jako dobu, která má uplynout od začátku (pokud je tento přepínač zapnutý).",
"app.examForm.errors.begin": "Začátek musí být platný časový záznam v budoucnosti.",
"app.examForm.errors.end": "Konec musí být platný časový záznam.",
"app.examForm.errors.endBeforeBegin": "Konec musí být později než začátek.",
"app.examForm.errors.endInPast": "Konec musí být v budoucnosti.",
"app.examForm.errors.length": "Délka musí být v platném formátu a větší než nula.",
"app.examForm.errors.tooLongExam": "Zkouška nesmí trvat déle než 24 hodin.",
"app.examForm.length": "Délka [h:mm]:",
"app.examForm.saveExam": "Uložit zkoušku",
"app.exercise.addReferenceSolutionDetailed": "Referenční řešení můžete vytvořit na hlavní stránce úlohy.",
"app.exercise.admins": "Administrátoři",
"app.exercise.admins.explanation": "Administrátoři mají stejná práva jako autor úlohy, ale nejsou zobrazováni v seznamech ani nejsou použiti při filtrování úloh.",
Expand Down Expand Up @@ -1034,6 +1049,18 @@
"app.groupDetail.supervisors": "Vedoucí skupiny {groupName}",
"app.groupDetail.threshold": "Minimální procentuální počet bodů potřebných ke splnění tohoto kurzu",
"app.groupDetail.title": "Zadané úlohy",
"app.groupExams.createExamExplain": "The exam group works like a regular group, but it has additional security features. The students does not see the assignments until they lock them selves in. The students can lock in only during the exam and will be unlocked afterwards. A locked student may access the system from a single IP address and may not visit any other groups.",
"app.groupExams.examButton": "Create Exam",
"app.groupExams.examGroupCreateTitle": "Change to an examination group",
"app.groupExams.examGroupTitle": "Examination group",
"app.groupExams.examModal.create": "Plan an examination in this group",
"app.groupExams.removeExam": "Cancel examination",
"app.groupExams.removeExamExplain": "TODO",
"app.groupExams.title": "Change Group Settings",
"app.groupExams.truncateExam": "End Now",
"app.groupExams.truncateExamExplain": "TODO",
"app.groupExams.updateExam": "Update",
"app.groupExams.updateExamExplain": "TODO",
"app.groupInfo.title": "Podrobnosti a metadata skupiny",
"app.groupInvitationForm.expireAt": "Konec platnosti:",
"app.groupInvitationForm.expireAtExplanation": "Odkaz pozvánky bude rozpoznatelný ReCodExem i po uplynutí doby platnosti, ale studenti jej nebudou moci použít.",
Expand All @@ -1059,6 +1086,9 @@
"app.groupTree.treeViewLeaf.archivedTooltip": "Tato skupina byla odsunuta do archivu",
"app.groupTree.treeViewLeaf.organizationalTooltip": "Skupina je organizační (nemá žádné studenty ani zadané úlohy)",
"app.groupTree.treeViewLeaf.publicTooltip": "Skupina je veřejná",
"app.groupTypeButton.exam": "Zkoušková",
"app.groupTypeButton.organizational": "Organizační",
"app.groupTypeButton.regular": "Běžná",
"app.groupUserSolutions.allAssignmentSolutions": "všechna řešení úlohy",
"app.groupUserSolutions.assignmentDetail": "zadaná úloha",
"app.groupUserSolutions.groupByAssignmentsCheckbox": "Seskupit podle úloh",
Expand Down Expand Up @@ -1220,9 +1250,6 @@
"app.numericTextField.validationFailedMax": "Hodnota nesmí být vyšší než {validateMax}.",
"app.numericTextField.validationFailedMin": "Hodnota nesmí být nižší než {validateMin}.",
"app.numericTextField.validationFailedMinMax": "Hodnota musí být v rozmezí {validateMin} a {validateMax}.",
"app.organizationalGroupButton.label": "Změnit typ na",
"app.organizationalGroupButton.set": "Organizační",
"app.organizationalGroupButton.unset": "Běžnou",
"app.page.failed": "Načtení stránky se nezdařilo",
"app.page.failedDescription.explain": "Tento problém mohl být způsoben výpadkem sítě nebo interní chybou na straně serveru. Rovněž je možné, že požadované datové objekty pro zobrazení této stránky byly smazány.",
"app.page.failedDescription.sorry": "Prosíme, zkuste to později. Omlouváme se za způsobené problémy. Pokud problém přetrvává ověřte, že zobrazovaný objekt stále existuje.",
Expand Down
33 changes: 30 additions & 3 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,11 @@
"app.editGroup.cannotDeleteGroupWithSubgroups": "Group with nested sub-groups cannot be deleted.",
"app.editGroup.cannotDeleteRootGroup": "This is a so-called root group and it cannot be deleted.",
"app.editGroup.changeGroupType": "Change group type",
"app.editGroup.currentType": "Current type",
"app.editGroup.deleteGroup": "Delete Group",
"app.editGroup.deleteGroupWarning": "Deleting a group will make all attached entities (assignments, solutions, ...) inaccessible.",
"app.editGroup.examButton": "Create Exam",
"app.editGroup.examExplain": "Exam groups work the same as regular groups internally. The exam flag is mainly an idicator for the users and it may also affect the way how the group is listed or when it is archived. This indicator is completely independent of the Exam terms which can be set on a so named page.",
"app.editGroup.organizationalExplain": "Regular groups are containers for students and assignments. Organizational groups are intended to create hierarchy, so they are forbidden to hold any students or assignments.",
"app.editGroup.relocateGroup": "Relocate Group",
"app.editGroup.title": "Change Group Settings",
Expand Down Expand Up @@ -713,6 +716,18 @@
"app.evaluationTable.evaluationIsDebug": "Evaluated in debug mode (complete logs and dumps)",
"app.evaluationTable.notAvailable": "Evaluation not available",
"app.evaluationTable.score": "Score:",
"app.examForm.beginImmediately": "Begin immediately",
"app.examForm.end": "End:",
"app.examForm.endRelative": "Set length (instead of explicit end)",
"app.examForm.endRelativeExplanation": "The end of the exam can be set as explicit date (default), or as a period from the beginning (if this checkbox is set).",
"app.examForm.errors.begin": "The beginning must be a valid time record in the future.",
"app.examForm.errors.end": "The end must be a valid time record.",
"app.examForm.errors.endBeforeBegin": "The end must be after beginning.",
"app.examForm.errors.endInPast": "The end must be in the future.",
"app.examForm.errors.length": "The length must be in valid format and not zero.",
"app.examForm.errors.tooLongExam": "The exam must not be longer than 24 hours.",
"app.examForm.length": "Length [h:mm]:",
"app.examForm.saveExam": "Save Exam",
"app.exercise.addReferenceSolutionDetailed": "A reference solution can be added on the exercise detail page.",
"app.exercise.admins": "Administrators",
"app.exercise.admins.explanation": "The administrators have the same permissions as the author towards the exercise, but they are not explicitly mentioned in listings or used in search filters.",
Expand Down Expand Up @@ -1034,6 +1049,18 @@
"app.groupDetail.supervisors": "Supervisors of {groupName}",
"app.groupDetail.threshold": "Minimum percent of the total points count needed to complete the course",
"app.groupDetail.title": "Group Assignments",
"app.groupExams.createExamExplain": "The exam group works like a regular group, but it has additional security features. The students does not see the assignments until they lock them selves in. The students can lock in only during the exam and will be unlocked afterwards. A locked student may access the system from a single IP address and may not visit any other groups.",
"app.groupExams.examButton": "Create Exam",
"app.groupExams.examGroupCreateTitle": "Change to an examination group",
"app.groupExams.examGroupTitle": "Examination group",
"app.groupExams.examModal.create": "Plan an examination in this group",
"app.groupExams.removeExam": "Cancel examination",
"app.groupExams.removeExamExplain": "TODO",
"app.groupExams.title": "Change Group Settings",
"app.groupExams.truncateExam": "End Now",
"app.groupExams.truncateExamExplain": "TODO",
"app.groupExams.updateExam": "Update",
"app.groupExams.updateExamExplain": "TODO",
"app.groupInfo.title": "Group Details and Metadata",
"app.groupInvitationForm.expireAt": "Expire at:",
"app.groupInvitationForm.expireAtExplanation": "An invitation link will be still recognized by ReCodEx after the expiration date, but the students will not be allowed to use it.",
Expand All @@ -1059,6 +1086,9 @@
"app.groupTree.treeViewLeaf.archivedTooltip": "The group is archived",
"app.groupTree.treeViewLeaf.organizationalTooltip": "The group is organizational (it does not have any students or assignments)",
"app.groupTree.treeViewLeaf.publicTooltip": "The group is public",
"app.groupTypeButton.exam": "Exam",
"app.groupTypeButton.organizational": "Organizational",
"app.groupTypeButton.regular": "Regular",
"app.groupUserSolutions.allAssignmentSolutions": "all assignment solutions",
"app.groupUserSolutions.assignmentDetail": "assignment detail",
"app.groupUserSolutions.groupByAssignmentsCheckbox": "Group by assignments",
Expand Down Expand Up @@ -1220,9 +1250,6 @@
"app.numericTextField.validationFailedMax": "The value must not be greater than {validateMax}.",
"app.numericTextField.validationFailedMin": "The value must not be lesser than {validateMin}.",
"app.numericTextField.validationFailedMinMax": "The value must be in between {validateMin} and {validateMax}.",
"app.organizationalGroupButton.label": "Change type to",
"app.organizationalGroupButton.set": "Organizational",
"app.organizationalGroupButton.unset": "Regular",
"app.page.failed": "Cannot load the page",
"app.page.failedDescription.explain": "This problem might have been caused by network failure or by internal error at server side. It is also possible that some of the resources required for displaying this page have been deleted.",
"app.page.failedDescription.sorry": "We are sorry for the inconvenience, please try again later. If the problem prevails, verify that the requested resource still exists.",
Expand Down

0 comments on commit 4f096a9

Please sign in to comment.