Skip to content

Commit 36a5642

Browse files
author
Martin Krulis
committed
SIS group unbinding implemented. Additionally, shortcut for deleting groups was added to SIS integration module.
1 parent 0df7548 commit 36a5642

File tree

8 files changed

+140
-34
lines changed

8 files changed

+140
-34
lines changed

src/containers/SisSupervisorGroupsContainer/SisSupervisorGroupsContainer.js

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { LinkContainer } from 'react-router-bootstrap';
1111

1212
import { fetchAllGroups, fetchGroupIfNeeded } from '../../redux/modules/groups';
1313
import { fetchSisStatusIfNeeded } from '../../redux/modules/sisStatus';
14-
import { fetchSisSupervisedCourses, sisCreateGroup, sisBindGroup } from '../../redux/modules/sisSupervisedCourses';
14+
import {
15+
fetchSisSupervisedCourses,
16+
sisCreateGroup,
17+
sisBindGroup,
18+
sisUnbindGroup,
19+
} from '../../redux/modules/sisSupervisedCourses';
1520
import { fetchSisPossibleParentsIfNeeded } from '../../redux/modules/sisPossibleParents';
1621
import { sisPossibleParentsSelector } from '../../redux/selectors/sisPossibleParents';
1722
import { sisStateSelector } from '../../redux/selectors/sisStatus';
@@ -23,11 +28,13 @@ import UsersNameContainer from '../UsersNameContainer';
2328
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
2429
import SisCreateGroupForm from '../../components/forms/SisCreateGroupForm';
2530
import SisBindGroupForm from '../../components/forms/SisBindGroupForm';
31+
import Confirm from '../../components/forms/Confirm';
2632
import { getGroupCanonicalLocalizedName } from '../../helpers/localizedData';
33+
import DeleteGroupButtonContainer from '../../containers/DeleteGroupButtonContainer';
2734

2835
import Icon, { GroupIcon } from '../../components/icons';
2936
import withLinks from '../../helpers/withLinks';
30-
import { unique, arrayToObject } from '../../helpers/common';
37+
import { unique, arrayToObject, hasPermissions } from '../../helpers/common';
3138

3239
const days = {
3340
cs: ['Po', 'Út', 'St', 'Čt', 'Pá', 'So', 'Ne'],
@@ -51,7 +58,7 @@ const getLocalizedData = (obj, locale) => {
5158

5259
const filterGroupsForBinding = (groups, alreadyBoundGroups) => {
5360
const bound = arrayToObject(alreadyBoundGroups);
54-
return groups.filter(group => !bound[group.id]);
61+
return groups.filter(group => !bound[group.id] && !group.organizational && !group.archived);
5562
};
5663

5764
class SisSupervisorGroupsContainer extends Component {
@@ -93,6 +100,7 @@ class SisSupervisorGroupsContainer extends Component {
93100
currentUserId,
94101
createGroup,
95102
bindGroup,
103+
unbindGroup,
96104
sisPossibleParents,
97105
groupsAccessor,
98106
links: { GROUP_INFO_URI_FACTORY, GROUP_DETAIL_URI_FACTORY },
@@ -213,6 +221,7 @@ class SisSupervisorGroupsContainer extends Component {
213221
<Table hover>
214222
<thead>
215223
<tr>
224+
<th className="shrink-col" />
216225
<th>
217226
<FormattedMessage id="generic.name" defaultMessage="Name" />
218227
</th>
@@ -228,6 +237,22 @@ class SisSupervisorGroupsContainer extends Component {
228237
<tbody>
229238
{course.groups.map((group, i) => (
230239
<tr key={i}>
240+
<td className="shrink-col">
241+
{group.organizational && (
242+
<OverlayTrigger
243+
placement="bottom"
244+
overlay={
245+
<Tooltip id={`hint:${course.course.code}:${group.id}`}>
246+
<FormattedMessage
247+
id="app.sisSupervisor.organizationalGroupWarning"
248+
defaultMessage="Students cannot join organizational groups."
249+
/>
250+
</Tooltip>
251+
}>
252+
<GroupIcon organizational gapRight />
253+
</OverlayTrigger>
254+
)}
255+
</td>
231256
<td>
232257
{getGroupCanonicalLocalizedName(group, groupsAccessor, locale)}
233258
</td>
@@ -237,22 +262,52 @@ class SisSupervisorGroupsContainer extends Component {
237262
))}
238263
</td>
239264
<td className="text-right">
240-
<span>
241-
<LinkContainer
242-
to={
243-
group.organizational
244-
? GROUP_INFO_URI_FACTORY(group.id)
245-
: GROUP_DETAIL_URI_FACTORY(group.id)
246-
}>
247-
<Button bsStyle="primary" bsSize="xs" className="btn-flat">
248-
<GroupIcon gapRight />
249-
<FormattedMessage
250-
id="app.group.detail"
251-
defaultMessage="Group Detail"
252-
/>
253-
</Button>
254-
</LinkContainer>
255-
</span>
265+
<LinkContainer
266+
to={
267+
group.organizational
268+
? GROUP_INFO_URI_FACTORY(group.id)
269+
: GROUP_DETAIL_URI_FACTORY(group.id)
270+
}>
271+
<Button bsStyle="primary" bsSize="xs">
272+
<GroupIcon gapRight />
273+
<FormattedMessage
274+
id="app.group.detail"
275+
defaultMessage="Group Detail"
276+
/>
277+
</Button>
278+
</LinkContainer>
279+
280+
<Confirm
281+
id={`${course.course.code}:${group.id}`}
282+
onConfirmed={() =>
283+
unbindGroup(
284+
course.course.code,
285+
group.id,
286+
currentUserId,
287+
term.year,
288+
term.term
289+
)
290+
}
291+
question={
292+
<FormattedMessage
293+
id="app.group.unbind.confirmQuestion"
294+
defaultMessage="Do you really wish to unbind the group? The group will linger on, but it will be detached from the SIS so the students will not see it."
295+
/>
296+
}>
297+
<Button bsStyle="danger" bsSize="xs">
298+
<Icon icon={['far', 'hand-scissors']} gapRight />
299+
<FormattedMessage
300+
id="app.group.unbind"
301+
defaultMessage="Unbind"
302+
/>
303+
</Button>
304+
</Confirm>
305+
306+
{hasPermissions(group, 'remove') &&
307+
group.parentGroupId !== null &&
308+
group.childGroups.length === 0 && (
309+
<DeleteGroupButtonContainer id={group.id} bsSize="xs" />
310+
)}
256311
</td>
257312
</tr>
258313
))}
@@ -339,6 +394,7 @@ SisSupervisorGroupsContainer.propTypes = {
339394
sisCourses: ImmutablePropTypes.map,
340395
createGroup: PropTypes.func.isRequired,
341396
bindGroup: PropTypes.func.isRequired,
397+
unbindGroup: PropTypes.func.isRequired,
342398
links: PropTypes.object,
343399
sisPossibleParents: ImmutablePropTypes.map,
344400
groupsAccessor: PropTypes.func.isRequired,
@@ -363,6 +419,8 @@ export default injectIntl(
363419
createGroup: (courseId, data, userId, year, term) =>
364420
dispatch(sisCreateGroup(courseId, data, userId, year, term)).then(() => dispatch(fetchAllGroups())),
365421
bindGroup: (courseId, data, userId, year, term) => dispatch(sisBindGroup(courseId, data, userId, year, term)),
422+
unbindGroup: (courseId, groupId, userId, year, term) =>
423+
dispatch(sisUnbindGroup(courseId, groupId, userId, year, term)),
366424
})
367425
)(SisSupervisorGroupsContainer)
368426
)

src/locales/cs.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,8 @@
786786
"app.group.spervisorsView.addStudent": "Přidat studenta",
787787
"app.group.spervisorsView.groupExercises": "Skupinové úlohy",
788788
"app.group.title": "Detaily skupiny",
789+
"app.group.unbind": "Odvázat",
790+
"app.group.unbind.confirmQuestion": "Opravdu si přejete zrušit tuto vazbu na skupinu? Skupina zůstane na svém místě, ale bez odpovídající vazby na SIS ji nemusí nalézt studenti.",
789791
"app.group.unsetRoot": "Zrušit výběr",
790792
"app.groupDetail.assignments": "Zadané úlohy",
791793
"app.groupDetail.bindings.genericProvider": "Externí vazby na '{provider}'",
@@ -1152,6 +1154,7 @@
11521154
"app.sisSupervisor.noAccessible": "Váš účet nepodporuje integraci se SISem. Přihlašte se prosím pomocí CAS-UK.",
11531155
"app.sisSupervisor.noSisGroups": "V současnosti nejsou v ReCodExu žádné skupiny, které by odpovídaly tomuto lístku ze SISu.",
11541156
"app.sisSupervisor.noUsersInNewGroupsWarning": "Upozornění: při založení nebo svázání skupiny s lístkem ze SISu nejsou do skupiny přidáni žádní studenti. Svázání pouze zajistí, že je skupina viditelná pro příslušné studenty, a že se do ní mohou sami přihlásit.",
1157+
"app.sisSupervisor.organizationalGroupWarning": "Studenti se nemohou přidat do organizační skupiny.",
11551158
"app.sisSupervisor.sisGroupsCreate": "Zakládání skupin asociovaných s kurzy ze SISu UK",
11561159
"app.sisSupervisor.sisGroupsCreateExplain": "SIS kurzy, které vyučujete v určitém semestru a které mají mapování do ReCodExu. Můžete vytvořit novou skupinu s vazbou na SIS, nebo svázat existující skupinu na tyto kurzy.",
11571160
"app.sisSupervisor.yearTerm": "Rok a semestr:",

src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,8 @@
786786
"app.group.spervisorsView.addStudent": "Add Student",
787787
"app.group.spervisorsView.groupExercises": "Group Exercises",
788788
"app.group.title": "Group detail",
789+
"app.group.unbind": "Unbind",
790+
"app.group.unbind.confirmQuestion": "Do you really wish to unbind the group? The group will linger on, but it will be detached from the SIS so the students will not see it.",
789791
"app.group.unsetRoot": "Unset",
790792
"app.groupDetail.assignments": "Assignments",
791793
"app.groupDetail.bindings.genericProvider": "External binding to '{provider}'",
@@ -1152,6 +1154,7 @@
11521154
"app.sisSupervisor.noAccessible": "Your account does not support SIS integration. Please, log in using CAS-UK.",
11531155
"app.sisSupervisor.noSisGroups": "Currently there are no ReCodEx groups matching this SIS course.",
11541156
"app.sisSupervisor.noUsersInNewGroupsWarning": "Please note that when a group is created from or bound to a SIS course, no students are added to this group. The binding process only ensures that the group is visible to the students and they are allowed to join it.",
1157+
"app.sisSupervisor.organizationalGroupWarning": "Students cannot join organizational groups.",
11551158
"app.sisSupervisor.sisGroupsCreate": "Create Groups Associated with UK SIS Courses",
11561159
"app.sisSupervisor.sisGroupsCreateExplain": "SIS courses you teach in particular semesters and which have mapping to ReCodEx. You may create new groups with binding or bind existing groups to these courses.",
11571160
"app.sisSupervisor.yearTerm": "Year and term:",

src/locales/whitelist_en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,8 @@
786786
"app.group.spervisorsView.addStudent",
787787
"app.group.spervisorsView.groupExercises",
788788
"app.group.title",
789+
"app.group.unbind",
790+
"app.group.unbind.confirmQuestion",
789791
"app.group.unsetRoot",
790792
"app.groupDetail.assignments",
791793
"app.groupDetail.bindings.genericProvider",
@@ -1152,6 +1154,7 @@
11521154
"app.sisSupervisor.noAccessible",
11531155
"app.sisSupervisor.noSisGroups",
11541156
"app.sisSupervisor.noUsersInNewGroupsWarning",
1157+
"app.sisSupervisor.organizationalGroupWarning",
11551158
"app.sisSupervisor.sisGroupsCreate",
11561159
"app.sisSupervisor.sisGroupsCreateExplain",
11571160
"app.sisSupervisor.yearTerm",

src/redux/modules/groups.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import createRecord from '../helpers/resourceManager/recordFactory';
99
import { resourceStatus } from '../helpers/resourceManager/status';
1010
import { actionTypes as assignmentsActionTypes } from './assignments';
1111
import { actionTypes as shadowAssignmentsActionTypes } from './shadowAssignments';
12-
import { actionTypes as sisSupervisedCoursesActionTypes } from './sisSupervisedCourses';
12+
import { actionTypes as sisSupervisedCoursesActionTypes } from './sisSupervisedCoursesTypes';
1313
import { selectedInstanceId } from '../selectors/auth';
1414

1515
import { objectMap } from '../../helpers/common';
@@ -378,6 +378,14 @@ const reducer = handleActions(
378378

379379
[sisSupervisedCoursesActionTypes.CREATE_FULFILLED]: (state, { payload: data }) =>
380380
state.setIn(['resources', data.id], createRecord({ state: resourceStatus.FULFILLED, data })),
381+
382+
[sisSupervisedCoursesActionTypes.BIND_FULFILLED]: (state, { payload: data }) =>
383+
state.setIn(['resources', data.id], createRecord({ state: resourceStatus.FULFILLED, data })),
384+
385+
[actionTypes.UNBIND_FULFILLED]: (state, { meta: { courseId, groupId } }) =>
386+
state.updateIn(['resources', groupId, 'data', 'privateData', 'bindings', 'sis'], bindings =>
387+
bindings.filter(binding => binding !== courseId)
388+
),
381389
}),
382390
initialState
383391
);

src/redux/modules/sisSupervisedCourses.js

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { handleActions } from 'redux-actions';
22
import factory, { initialState, createRecord, resourceStatus } from '../helpers/resourceManager';
33
import { createApiAction } from '../middleware/apiMiddleware';
44
import { fromJS } from 'immutable';
5-
5+
import { actionTypes } from './sisSupervisedCoursesTypes';
6+
import { actionTypes as groupsActionTypes } from './groups';
67
/**
78
* Create actions & reducer
89
*/
@@ -12,17 +13,6 @@ const { reduceActions } = factory({
1213
resourceName,
1314
});
1415

15-
export const actionTypes = {
16-
FETCH: 'recodex/sisSupervisedCourses/FETCH',
17-
FETCH_PENDING: 'recodex/sisSupervisedCourses/FETCH_PENDING',
18-
FETCH_REJECTED: 'recodex/sisSupervisedCourses/FETCH_REJECTED',
19-
FETCH_FULFILLED: 'recodex/sisSupervisedCourses/FETCH_FULFILLED',
20-
CREATE: 'recodex/sisSupervisedCourses/CREATE',
21-
CREATE_FULFILLED: 'recodex/sisSupervisedCourses/CREATE_FULFILLED',
22-
BIND: 'recodex/sisSupervisedCourses/BIND',
23-
BIND_FULFILLED: 'recodex/sisSupervisedCourses/BIND_FULFILLED',
24-
};
25-
2616
export const fetchSisSupervisedCourses = (userId, year, term) =>
2717
createApiAction({
2818
type: actionTypes.FETCH,
@@ -49,18 +39,33 @@ export const sisBindGroup = (courseId, data, userId, year, term) =>
4939
body: { ...data },
5040
});
5141

42+
export const sisUnbindGroup = (courseId, groupId, userId, year, term) =>
43+
createApiAction({
44+
type: actionTypes.UNBIND,
45+
method: 'DELETE',
46+
endpoint: `/extensions/sis/remote-courses/${courseId}/bindings/${groupId}`,
47+
meta: { courseId, groupId, userId, year, term },
48+
});
49+
5250
const reducer = handleActions(
5351
Object.assign({}, reduceActions, {
5452
[actionTypes.CREATE_FULFILLED]: (state, { meta: { userId, courseId, year, term }, payload }) =>
5553
state.updateIn(['resources', userId, `${year}-${term}`, 'data', courseId, 'groups'], groups =>
5654
groups.push(fromJS(payload))
5755
),
5856

59-
[actionTypes.BIND_FULFILLED]: (state, { meta: { userId, courseId, year, term }, payload }) =>
57+
[actionTypes.BIND_FULFILLED]: (state, { meta: { courseId, userId, year, term }, payload }) =>
6058
state.updateIn(['resources', userId, `${year}-${term}`, 'data', courseId, 'groups'], groups =>
6159
groups.push(fromJS(payload))
6260
),
6361

62+
[actionTypes.UNBIND_FULFILLED]: (state, { meta: { courseId, groupId, userId } }) =>
63+
state.updateIn(['resources', userId], terms =>
64+
terms.map(term =>
65+
term.updateIn(['data', courseId, 'groups'], groups => groups.filter(group => group.get('id') !== groupId))
66+
)
67+
),
68+
6469
[actionTypes.FETCH_PENDING]: (state, { meta: { userId, year, term } }) =>
6570
state.setIn(['resources', userId, `${year}-${term}`], createRecord()),
6671

@@ -80,6 +85,19 @@ const reducer = handleActions(
8085
),
8186
})
8287
),
88+
89+
[groupsActionTypes.REMOVE_FULFILLED]: (state, { meta: { id: groupId } }) =>
90+
state.update('resources', users =>
91+
users.map(userTerms =>
92+
userTerms.map(term =>
93+
term.update('data', courses =>
94+
courses.map(course =>
95+
course.update('groups', groups => groups.filter(group => group.get('id') !== groupId))
96+
)
97+
)
98+
)
99+
)
100+
),
83101
}),
84102
initialState
85103
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// action types declaration was moved outside the auth module to break cyclic import dependencies
2+
export const actionTypes = {
3+
FETCH: 'recodex/sisSupervisedCourses/FETCH',
4+
FETCH_PENDING: 'recodex/sisSupervisedCourses/FETCH_PENDING',
5+
FETCH_REJECTED: 'recodex/sisSupervisedCourses/FETCH_REJECTED',
6+
FETCH_FULFILLED: 'recodex/sisSupervisedCourses/FETCH_FULFILLED',
7+
CREATE: 'recodex/sisSupervisedCourses/CREATE',
8+
CREATE_FULFILLED: 'recodex/sisSupervisedCourses/CREATE_FULFILLED',
9+
BIND: 'recodex/sisSupervisedCourses/BIND',
10+
BIND_FULFILLED: 'recodex/sisSupervisedCourses/BIND_FULFILLED',
11+
UNBIND: 'recodex/sisSupervisedCourses/UNBIND',
12+
UNBIND_FULFILLED: 'recodex/sisSupervisedCourses/UNBIND_FULFILLED',
13+
};

src/redux/modules/users.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import factory, { initialState, createRecord, resourceStatus } from '../helpers/
55
import { createApiAction } from '../middleware/apiMiddleware';
66

77
import { additionalActionTypes as groupsActionTypes } from './groups';
8-
import { actionTypes as sisSupervisedCoursesActionTypes } from './sisSupervisedCourses';
8+
import { actionTypes as sisSupervisedCoursesActionTypes } from './sisSupervisedCoursesTypes';
99
import { actionTypes as emailVerificationActionTypes } from './emailVerification';
1010
import { actionTypes as paginationActionTypes } from './pagination';
1111
import { actionTypes as exercisesAuthorsActionTypes } from './exercisesAuthors';

0 commit comments

Comments
 (0)