diff --git a/src/components/Groups/GroupExamsTable/GroupExamsTable.js b/src/components/Groups/GroupExamsTable/GroupExamsTable.js
index 6cb90d22c..1ad53e57c 100644
--- a/src/components/Groups/GroupExamsTable/GroupExamsTable.js
+++ b/src/components/Groups/GroupExamsTable/GroupExamsTable.js
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Table } from 'react-bootstrap';
+import { Link } from 'react-router-dom';
import { defaultMemoize } from 'reselect';
import DateTime from '../../widgets/DateTime';
@@ -10,12 +11,12 @@ import { VisibleIcon } from '../../icons';
const sortExams = defaultMemoize(exams => {
const sorted = [...exams];
- return sorted.sort((a, b) => a.end - b.end);
+ return sorted.sort((a, b) => a.end - b.end || a.begin - b.begin);
});
-const GroupExamsTable = ({ exams = null, selected = null, setSelected = null }) =>
+const GroupExamsTable = ({ exams = null, selected = null, linkFactory = null }) =>
exams && exams.length > 0 ? (
-
+
|
@@ -28,12 +29,12 @@ const GroupExamsTable = ({ exams = null, selected = null, setSelected = null })
|
- {setSelected && | }
+ {linkFactory && | }
{sortExams(exams).map((exam, idx) => (
-
+
#{idx + 1} |
@@ -50,16 +51,21 @@ const GroupExamsTable = ({ exams = null, selected = null, setSelected = null })
)}
|
- {setSelected && (
-
-
+ {linkFactory && (
+ |
+
+
+
|
)}
@@ -69,7 +75,10 @@ const GroupExamsTable = ({ exams = null, selected = null, setSelected = null })
) : (
-
+
);
@@ -77,7 +86,7 @@ const GroupExamsTable = ({ exams = null, selected = null, setSelected = null })
GroupExamsTable.propTypes = {
exams: PropTypes.array,
selected: PropTypes.string,
- setSelected: PropTypes.func,
+ linkFactory: PropTypes.func,
};
export default GroupExamsTable;
diff --git a/src/components/Groups/LockedStudentsTable/LockedStudentsTable.js b/src/components/Groups/LockedStudentsTable/LockedStudentsTable.js
new file mode 100644
index 000000000..5494b6f40
--- /dev/null
+++ b/src/components/Groups/LockedStudentsTable/LockedStudentsTable.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import { Table } from 'react-bootstrap';
+import { defaultMemoize } from 'reselect';
+
+import UsersName from '../../Users/UsersName';
+import ExamUnlockButtonContainer from '../../../containers/ExamUnlockButtonContainer';
+import { createUserNameComparator } from '../../helpers/users';
+
+const sortStudents = defaultMemoize((lockedStudents, locale) => {
+ const sorted = [...lockedStudents];
+ return sorted.sort(createUserNameComparator(locale));
+});
+
+const LockedStudentsTable = ({ groupId, lockedStudents, currentUser, intl: { locale } }) =>
+ lockedStudents && lockedStudents.length > 0 ? (
+
+
+ {sortStudents(lockedStudents).map(student => (
+
+
+
+ |
+
+
+ |
+
+ ))}
+
+
+ ) : (
+
+
+
+
+
+ );
+
+LockedStudentsTable.propTypes = {
+ groupId: PropTypes.string.isRequired,
+ lockedStudents: PropTypes.array,
+ currentUser: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ }),
+ intl: PropTypes.object.isRequired,
+};
+
+export default injectIntl(LockedStudentsTable);
diff --git a/src/components/Groups/LockedStudentsTable/index.js b/src/components/Groups/LockedStudentsTable/index.js
new file mode 100644
index 000000000..a1e325ff5
--- /dev/null
+++ b/src/components/Groups/LockedStudentsTable/index.js
@@ -0,0 +1,2 @@
+import LockedStudentsTable from './LockedStudentsTable';
+export default LockedStudentsTable;
diff --git a/src/components/buttons/ExamUnlockButton/ExamUnlockButton.js b/src/components/buttons/ExamUnlockButton/ExamUnlockButton.js
new file mode 100644
index 000000000..654f8edb3
--- /dev/null
+++ b/src/components/buttons/ExamUnlockButton/ExamUnlockButton.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { UnlockIcon, LoadingIcon } from '../../icons';
+import Button from '../../widgets/TheButton';
+
+const ExamUnlockButton = ({ pending, disabled = false, unlockUserForExam, ...props }) => (
+
+);
+
+ExamUnlockButton.propTypes = {
+ pending: PropTypes.bool.isRequired,
+ disabled: PropTypes.bool,
+ unlockUserForExam: PropTypes.func.isRequired,
+};
+
+export default ExamUnlockButton;
diff --git a/src/components/buttons/ExamUnlockButton/index.js b/src/components/buttons/ExamUnlockButton/index.js
new file mode 100644
index 000000000..619cece2c
--- /dev/null
+++ b/src/components/buttons/ExamUnlockButton/index.js
@@ -0,0 +1,2 @@
+import ExamUnlockButton from './ExamUnlockButton';
+export default ExamUnlockButton;
diff --git a/src/components/layout/Navigation/Navigation.js b/src/components/layout/Navigation/Navigation.js
index c77a0b793..3bdc882d4 100644
--- a/src/components/layout/Navigation/Navigation.js
+++ b/src/components/layout/Navigation/Navigation.js
@@ -14,8 +14,18 @@ import { AssignmentIcon, ExerciseIcon, PipelineIcon, ShadowAssignmentIcon } from
import styles from './Navigation.less';
-const NavigationLink = ({ link, href, caption, icon = null, location: { pathname, search }, className }) =>
- link === pathname + search ? (
+const defaultLinkMatch = (link, pathname, search) => link === pathname + search;
+
+const NavigationLink = ({
+ link,
+ href,
+ caption,
+ icon = null,
+ location: { pathname, search },
+ className,
+ match = defaultLinkMatch,
+}) =>
+ match(link, pathname, search) ? (
{icon}
{caption}
@@ -38,6 +48,7 @@ NavigationLink.propTypes = {
caption: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
icon: PropTypes.element,
className: PropTypes.string,
+ match: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
search: PropTypes.string.isRequired,
diff --git a/src/components/layout/Navigation/linkCreators.js b/src/components/layout/Navigation/linkCreators.js
index 13a61f001..cf603f811 100644
--- a/src/components/layout/Navigation/linkCreators.js
+++ b/src/components/layout/Navigation/linkCreators.js
@@ -49,6 +49,7 @@ export const createGroupLinks = (
caption: ,
link: GROUP_EXAMS_URI_FACTORY(groupId),
icon: ,
+ match: (link, pathname) => pathname.startsWith(link),
},
];
diff --git a/src/containers/ExamUnlockButtonContainer/ExamUnlockButtonContainer.js b/src/containers/ExamUnlockButtonContainer/ExamUnlockButtonContainer.js
new file mode 100644
index 000000000..e4d85e6bf
--- /dev/null
+++ b/src/containers/ExamUnlockButtonContainer/ExamUnlockButtonContainer.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { injectIntl } from 'react-intl';
+import { defaultMemoize } from 'reselect';
+
+import ExamUnlockButton from '../../components/buttons/ExamUnlockButton';
+import { unlockStudentFromExam } from '../../redux/modules/groups';
+import { groupPendingUserUnlock } from '../../redux/selectors/groups';
+import { loggedInUserSelector } from '../../redux/selectors/users';
+import { getErrorMessage } from '../../locales/apiErrorMessages';
+import { addNotification } from '../../redux/modules/notifications';
+
+const unlockStudentHandlingErrors = defaultMemoize(
+ (unlockStudentFromExam, addNotification, formatMessage) => () =>
+ unlockStudentFromExam().catch(err => {
+ addNotification(getErrorMessage(formatMessage)(err), false);
+ })
+);
+
+const ExamUnlockButtonContainer = ({
+ groupId,
+ userId,
+ pending = null,
+ unlockStudentFromExam,
+ addNotification,
+ intl: { formatMessage },
+ ...props
+}) => (
+
+);
+
+ExamUnlockButtonContainer.propTypes = {
+ groupId: PropTypes.string.isRequired,
+ userId: PropTypes.string.isRequired,
+ pending: PropTypes.string,
+ unlockStudentFromExam: PropTypes.func.isRequired,
+ addNotification: PropTypes.func.isRequired,
+ intl: PropTypes.object,
+};
+
+const mapStateToProps = (state, { groupId }) => ({
+ pending: groupPendingUserUnlock(state, groupId),
+ currentUser: loggedInUserSelector(state),
+});
+
+const mapDispatchToProps = (dispatch, { groupId, userId }) => ({
+ unlockStudentFromExam: () => dispatch(unlockStudentFromExam(groupId, userId)),
+ addNotification: (...args) => dispatch(addNotification(...args)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ExamUnlockButtonContainer));
diff --git a/src/containers/ExamUnlockButtonContainer/index.js b/src/containers/ExamUnlockButtonContainer/index.js
new file mode 100644
index 000000000..9d20945fd
--- /dev/null
+++ b/src/containers/ExamUnlockButtonContainer/index.js
@@ -0,0 +1,2 @@
+import ExamUnlockButtonContainer from './ExamUnlockButtonContainer';
+export default ExamUnlockButtonContainer;
diff --git a/src/locales/cs.json b/src/locales/cs.json
index 45be72440..4987e1266 100644
--- a/src/locales/cs.json
+++ b/src/locales/cs.json
@@ -1084,18 +1084,21 @@
"app.groupExams.lockedStudentInfoStrict": "K ostatním skupinám nemáte přístup dokud jste v uzamčeném režimu.",
"app.groupExams.locking": "Typ zámku",
"app.groupExams.noExam": "V tuto chvíli není naplánovaná žádná zkouška",
- "app.groupExams.noPreviousExams": "Dosud nebyly žádné zkouškové termíny.",
"app.groupExams.pending.studentLockedTitle": "Jste uzamčeném režimu pro probíhající zkoušku",
"app.groupExams.pending.teacherInfo": "V tuto chvíli jsou zkouškové úlohy viditelné pouze studentům, kteří se uzamkli ve skupině.",
"app.groupExams.studentInfo": "Musíte se uzamknout ve skupině, abyste mohl(a) vidět zkouškové úlohy. Po čas uzamčení je komunikace s vámi omezena na vaši aktuální IP adresu.",
"app.groupExams.studentInfoRegular": "Navíc budete moct přistupovat k ostatním skupinám pouze v režimu pro čtení dokud budete v uzamčeném režimu.",
"app.groupExams.studentInfoStrict": "Navíc nebudete moct přistupovat k ostatním skupinám dokud budete v uzamčeném režimu.",
+ "app.groupExams.studentsBoxTitle": "Studenti účastnící se zkoušky",
"app.groupExams.timeAccuracyWarning": "Lokální hodiny na vašem systému musí být dostatečně seřízené, jinak nemusí tato komponenta fungovat zcela správně.",
"app.groupExams.title": "Zkouškové termíny skupiny",
+ "app.groupExams.unlockStudentButton": "Odemknout",
"app.groupExamsTable.begin": "Začala",
"app.groupExamsTable.end": "Skončila",
"app.groupExamsTable.lockType": "Typ zámku",
+ "app.groupExamsTable.noPreviousExams": "Dosud nebyly žádné zkouškové termíny.",
"app.groupExamsTable.selectButton": "Detail",
+ "app.groupExamsTable.unselectButton": "Zrušit",
"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.",
@@ -1202,6 +1205,7 @@
"app.localizedTexts.noText": "Pro danou lokalizaci není vyplněn ani text ani externí odkaz na zadaní. Tato úloha ještě není řádně dospecifikována.",
"app.localizedTexts.studentHintHeading": "Nápověda",
"app.localizedTexts.validation.noLocalizedText": "Prosíme povolte alespoň jednu záložku s lokalizovanými texty.",
+ "app.lockedStudentsTable.noLockedStudents": "Dosud se žádní studenti nezamkli ve skupině.",
"app.login.alreadyLoggedIn": "Již jste úspěšně přihlášen(a).",
"app.login.cannotRememberPassword": "Zapomněli jste heslo?",
"app.login.loginIsRequired": "Cílová stránka je dostupná pouze pro přihlášené uživatele. Nejprve je potřeba se přihlásit.",
diff --git a/src/locales/en.json b/src/locales/en.json
index 1d02de1b5..1054ef05e 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1084,18 +1084,21 @@
"app.groupExams.lockedStudentInfoStrict": "You may not access any other groups until the exam lock expires.",
"app.groupExams.locking": "Lock type",
"app.groupExams.noExam": "There is currently no exam scheduled",
- "app.groupExams.noPreviousExams": "There are no previous exams recorded.",
"app.groupExams.pending.studentLockedTitle": "You are locked in for an exam",
"app.groupExams.pending.teacherInfo": "The exam assignments are currently visible only to students who have lock themselves in the group.",
"app.groupExams.studentInfo": "You need to lock yourself in to see the exam assignments. When locked, your actions will be restricted to your current IP address.",
"app.groupExams.studentInfoRegular": "Furthermore, you will be able to access other groups in a read-only mode until the exam lock expires.",
"app.groupExams.studentInfoStrict": "Furthermore, you will not be able to access other groups until the exam lock expires.",
+ "app.groupExams.studentsBoxTitle": "Participating students",
"app.groupExams.timeAccuracyWarning": "Your local system clock should be sufficiently synchronized or this component may not work properly.",
"app.groupExams.title": "Group Exam Terms",
+ "app.groupExams.unlockStudentButton": "Unlock",
"app.groupExamsTable.begin": "Begun at",
"app.groupExamsTable.end": "Ended at",
"app.groupExamsTable.lockType": "Lock type",
+ "app.groupExamsTable.noPreviousExams": "There are no previous exams recorded.",
"app.groupExamsTable.selectButton": "Detail",
+ "app.groupExamsTable.unselectButton": "Unselect",
"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.",
@@ -1202,6 +1205,7 @@
"app.localizedTexts.noText": "There is no text nor link for given localization. The exercise is not fully specified yet.",
"app.localizedTexts.studentHintHeading": "Hint",
"app.localizedTexts.validation.noLocalizedText": "Please enable at least one tab of localized texts.",
+ "app.lockedStudentsTable.noLockedStudents": "There are no locked students yet.",
"app.login.alreadyLoggedIn": "You are already logged in.",
"app.login.cannotRememberPassword": "You cannot remember what your password was?",
"app.login.loginIsRequired": "Target page is available for authorized users only. Please sign in first.",
diff --git a/src/locales/whitelist_cs.json b/src/locales/whitelist_cs.json
index 0f6512500..2fe335df0 100644
--- a/src/locales/whitelist_cs.json
+++ b/src/locales/whitelist_cs.json
@@ -163,5 +163,6 @@
"app.systemMessagesList.text",
"generic.detail",
"generic.email",
- "generic.role"
-]
\ No newline at end of file
+ "generic.role",
+ "app.groupExamsTable.selectButton"
+]
diff --git a/src/pages/GroupExams/GroupExams.js b/src/pages/GroupExams/GroupExams.js
index 65c852701..8b98b570c 100644
--- a/src/pages/GroupExams/GroupExams.js
+++ b/src/pages/GroupExams/GroupExams.js
@@ -11,18 +11,36 @@ import { GroupNavigation } from '../../components/layout/Navigation';
import Box from '../../components/widgets/Box';
import GroupArchivedWarning from '../../components/Groups/GroupArchivedWarning';
import GroupExamsTable from '../../components/Groups/GroupExamsTable';
+import GroupExamStatus from '../../components/Groups/GroupExamStatus';
+import LockedStudentsTable from '../../components/Groups/LockedStudentsTable';
import { GroupExamsIcon } from '../../components/icons';
import { fetchGroup, fetchGroupIfNeeded, setExamPeriod, removeExamPeriod } from '../../redux/modules/groups';
+import { fetchByIds } from '../../redux/modules/users';
import { addNotification } from '../../redux/modules/notifications';
import { groupSelector, groupDataAccessorSelector, groupTypePendingChange } from '../../redux/selectors/groups';
+import { lockedStudentsOfGroupSelector } from '../../redux/selectors/usersGroups';
import { loggedInUserIdSelector } from '../../redux/selectors/auth';
import { isLoggedAsSuperAdmin, loggedInUserSelector } from '../../redux/selectors/users';
import withLinks from '../../helpers/withLinks';
-import GroupExamStatus from '../../components/Groups/GroupExamStatus';
+import { hasPermissions, safeGet } from '../../helpers/common';
+
+const isExam = ({ privateData: { examBegin, examEnd } }) => {
+ const now = Date.now() / 1000;
+ return examBegin && examEnd && examEnd > now && examBegin <= now;
+};
class GroupExams extends Component {
+ static loadAsync = ({ groupId }, dispatch) =>
+ Promise.all([
+ dispatch(fetchGroupIfNeeded(groupId)).then(({ value: group }) =>
+ hasPermissions(group, 'viewStudents')
+ ? dispatch(fetchByIds(safeGet(group, ['privateData', 'students']) || []))
+ : Promise.resolve()
+ ),
+ ]);
+
componentDidMount() {
this.props.loadAsync();
}
@@ -33,9 +51,21 @@ class GroupExams extends Component {
}
}
+ linkFactory = id => {
+ const {
+ params: { groupId, examId = null },
+ links: { GROUP_EXAMS_URI_FACTORY, GROUP_EXAMS_SPECIFIC_EXAM_URI_FACTORY },
+ } = this.props;
+ return String(id) === String(examId)
+ ? GROUP_EXAMS_URI_FACTORY(groupId)
+ : GROUP_EXAMS_SPECIFIC_EXAM_URI_FACTORY(groupId, id);
+ };
+
render() {
const {
+ params: { examId = null },
group,
+ lockedStudents,
currentUser,
groupsAccessor,
examBeginImmediately,
@@ -75,10 +105,37 @@ class GroupExams extends Component {
title={}
noPadding
unlimitedHeight>
-
+
+
+ {(examId || isExam(group)) && hasPermissions(group, 'viewStudents', 'setExamPeriod') && (
+
+
+
+ }
+ noPadding
+ unlimitedHeight>
+ {isExam(group) ? (
+
+ ) : (
+ examId
+ )}
+
+
+
+ )}
)}
@@ -92,9 +149,11 @@ GroupExams.propTypes = {
reload: PropTypes.func.isRequired,
params: PropTypes.shape({
groupId: PropTypes.string.isRequired,
+ examId: PropTypes.number,
}).isRequired,
group: ImmutablePropTypes.map,
currentUser: ImmutablePropTypes.map,
+ lockedStudents: PropTypes.array,
groupsAccessor: PropTypes.func.isRequired,
isSuperAdmin: PropTypes.bool,
examBeginImmediately: PropTypes.bool,
@@ -112,6 +171,7 @@ export default withLinks(
(state, { params: { groupId } }) => ({
group: groupSelector(state, groupId),
groupsAccessor: groupDataAccessorSelector(state),
+ lockedStudents: lockedStudentsOfGroupSelector(state, groupId),
userId: loggedInUserIdSelector(state),
currentUser: loggedInUserSelector(state),
isSuperAdmin: isLoggedAsSuperAdmin(state),
@@ -119,8 +179,8 @@ export default withLinks(
examEndRelative: examFormSelector(state, 'endRelative'),
examPendingChange: groupTypePendingChange(state, groupId),
}),
- (dispatch, { params: { groupId } }) => ({
- loadAsync: () => dispatch(fetchGroupIfNeeded(groupId)),
+ (dispatch, { params: { groupId, examId } }) => ({
+ loadAsync: () => GroupExams.loadAsync({ groupId, examId }, dispatch),
reload: () => dispatch(fetchGroup(groupId)),
addNotification: (...args) => dispatch(addNotification(...args)),
setExamPeriod: (begin, end, strict) => dispatch(setExamPeriod(groupId, begin, end, strict)),
diff --git a/src/pages/routes.js b/src/pages/routes.js
index 5b47d332f..ec2afe84e 100644
--- a/src/pages/routes.js
+++ b/src/pages/routes.js
@@ -173,6 +173,7 @@ const routesDescriptors = [
r('app/group/:groupId/assignments', GroupAssignments, 'GROUP_ASSIGNMENTS_URI_FACTORY', true),
r('app/group/:groupId/students', GroupStudents, 'GROUP_STUDENTS_URI_FACTORY', true),
r('app/group/:groupId/exams', GroupExams, 'GROUP_EXAMS_URI_FACTORY', true),
+ r('app/group/:groupId/exams/:examId', GroupExams, 'GROUP_EXAMS_SPECIFIC_EXAM_URI_FACTORY', true),
r('app/group/:groupId/user/:userId', GroupUserSolutions, 'GROUP_USER_SOLUTIONS_URI_FACTORY', true),
r('app/instance/:instanceId', Instance, 'INSTANCE_URI_FACTORY', true),
r('app/users', Users, 'USERS_URI', true),
diff --git a/src/redux/modules/users.js b/src/redux/modules/users.js
index 5cf978686..ce2eb9c35 100644
--- a/src/redux/modules/users.js
+++ b/src/redux/modules/users.js
@@ -253,6 +253,9 @@ const reducer = handleActions(
[groupActionTypes.LOCK_STUDENT_EXAM_FULFILLED]: (state, { payload: { user } }) =>
state.setIn(['resources', user.id], createRecord({ state: resourceStatus.FULFILLED, data: user })),
+
+ [groupActionTypes.UNLOCK_STUDENT_EXAM_FULFILLED]: (state, { payload }) =>
+ state.setIn(['resources', payload.id], createRecord({ state: resourceStatus.FULFILLED, data: payload })),
}),
initialState
);
diff --git a/src/redux/selectors/groups.js b/src/redux/selectors/groups.js
index d985a2b3a..7d59265ce 100644
--- a/src/redux/selectors/groups.js
+++ b/src/redux/selectors/groups.js
@@ -79,6 +79,11 @@ export const groupPendingUserLock = createSelector(
(groups, id) => groups && groups.getIn([id, 'pending-user-lock'], null)
);
+export const groupPendingUserUnlock = createSelector(
+ [groupsSelector, getParam],
+ (groups, id) => groups && groups.getIn([id, 'pending-user-unlock'], null)
+);
+
export const groupArchivedPendingChange = createSelector(
[groupsSelector, getParam],
(groups, id) => groups && groups.getIn([id, 'pending-archived'], false)
diff --git a/src/redux/selectors/usersGroups.js b/src/redux/selectors/usersGroups.js
index 9ae9f41a4..84c65a25c 100644
--- a/src/redux/selectors/usersGroups.js
+++ b/src/redux/selectors/usersGroups.js
@@ -78,6 +78,22 @@ export const studentsOfGroupSelector = createSelector(
}
);
+export const lockedStudentsOfGroupSelector = createSelector(
+ [readyUsersDataSelector, getParam, getState],
+ (users, groupId, state) => {
+ const studentIds = studentsIdsOfGroup(groupId)(state);
+ const studentsIndex = new Set(studentIds);
+ const now = Date.now() / 1000;
+ return users.filter(
+ user =>
+ user.privateData &&
+ user.privateData.groupLock === groupId &&
+ (!user.privateData.groupLockExpiration || user.privateData.groupLockExpiration <= now) &&
+ studentsIndex.has(user.id)
+ );
+ }
+);
+
// quite inefficient methods for filtering relevant groups (use carefully, maybe we will replace them in the future)
export const studentOfSelector = userId =>
createSelector(groupsSelector, groups =>