Skip to content

Commit 30b575d

Browse files
riccqimartin-henzchownces
authored
Achievement comments refactor to support avenger flow (#2252)
* major refactor to achievement feedback page * fix formatting issues * revert linting rules * use typed selector * fix boolean condition on crid Co-authored-by: Martin Henz <henz@comp.nus.edu.sg> Co-authored-by: En Rong <53928333+chownces@users.noreply.github.com>
1 parent bf4c5c5 commit 30b575d

File tree

10 files changed

+174
-66
lines changed

10 files changed

+174
-66
lines changed

src/commons/achievement/AchievementCommentCard.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { useMemo } from 'react';
22

33
import { Assessment } from '../assessment/AssessmentTypes';
44
import { history } from '../utils/HistoryHelper';
5+
import { useTypedSelector } from '../utils/Hooks';
56
import { showWarningMessage } from '../utils/NotificationsHelper';
67
import { assessmentTypeLink } from '../utils/ParamParseHelper';
78

8-
export type OwnProps = {
9+
const AchievementCommentCard = ({
10+
assessment,
11+
showToQuestion
12+
}: {
913
assessment: Assessment;
10-
courseId?: number;
11-
};
12-
13-
const AchievementCommentCard = ({ assessment, courseId }: OwnProps) => {
14+
showToQuestion: boolean;
15+
}) => {
16+
const courseId = useTypedSelector(store => store.session.courseId);
1417
const toMission = useMemo(
1518
() => (questionId: number) => {
1619
if (!courseId) {
@@ -37,13 +40,15 @@ const AchievementCommentCard = ({ assessment, courseId }: OwnProps) => {
3740
</span>
3841

3942
<div className="box-comment">
40-
<p>{question.comments === null ? 'Not Graded' : question.comments}</p>
43+
<p>{question.comments === null ? 'No Comments' : question.comments}</p>
4144
<p className="xp">{'XP: ' + question.xp + '/' + question.maxXp}</p>
4245
</div>
4346

44-
<button className="to-assessment-button" onClick={() => toMission(index)}>
45-
{'To Question'}
46-
</button>
47+
{showToQuestion && (
48+
<button className="to-assessment-button" onClick={() => toMission(index)}>
49+
{'To Question'}
50+
</button>
51+
)}
4752
</div>
4853
))}
4954
</div>

src/commons/achievement/AchievementOverview.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1+
import { useEffect } from 'react';
2+
import { useDispatch } from 'react-redux';
3+
import { AchievementUser } from 'src/features/achievement/AchievementTypes';
4+
5+
import { FETCH_TOTAL_XP, FETCH_TOTAL_XP_ADMIN } from '../application/types/SessionTypes';
16
import { useTypedSelector } from '../utils/Hooks';
27
import AchievementLevel from './overview/AchievementLevel';
38

49
type AchievementOverviewProps = {
510
name: string;
11+
userState: [AchievementUser | undefined, any];
612
};
713

814
function AchievementOverview(props: AchievementOverviewProps) {
9-
const { name } = props;
15+
const { name, userState } = props;
16+
const [selectedUser] = userState;
17+
const crid = selectedUser?.courseRegId;
18+
const userCrid = useTypedSelector(store => store.session.courseRegId);
19+
20+
const dispatch = useDispatch();
21+
useEffect(() => {
22+
// If user is student, fetch assessment details from assessment route instead, as seen below
23+
if (crid && crid !== userCrid) {
24+
dispatch({ type: FETCH_TOTAL_XP_ADMIN, payload: crid });
25+
} else {
26+
dispatch({ type: FETCH_TOTAL_XP });
27+
}
28+
}, [crid, userCrid, dispatch]);
1029

1130
const studentXp = useTypedSelector(store => store.session.xp);
1231

src/commons/achievement/AchievementView.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
getAbilityBackground,
99
getAbilityGlow
1010
} from '../../features/achievement/AchievementConstants';
11-
import { AchievementStatus } from '../../features/achievement/AchievementTypes';
12-
import { FETCH_ASSESSMENT } from '../application/types/SessionTypes';
13-
import { Assessment, FETCH_ASSESSMENT_OVERVIEWS } from '../assessment/AssessmentTypes';
11+
import { AchievementStatus, AchievementUser } from '../../features/achievement/AchievementTypes';
12+
import { FETCH_ASSESSMENT, FETCH_ASSESSMENT_ADMIN } from '../application/types/SessionTypes';
13+
import { FETCH_ASSESSMENT_OVERVIEWS } from '../assessment/AssessmentTypes';
14+
import { Assessment } from '../assessment/AssessmentTypes';
1415
import { useTypedSelector } from '../utils/Hooks';
1516
import AchievementCommentCard from './AchievementCommentCard';
1617
import { prettifyDate } from './utils/DateHelper';
@@ -19,25 +20,41 @@ import AchievementViewGoal from './view/AchievementViewGoal';
1920

2021
type AchievementViewProps = {
2122
focusUuid: string;
22-
courseRegId?: number;
23+
assessments?: Map<number, Assessment>;
24+
userState?: [AchievementUser | undefined, any];
2325
};
2426

25-
function AchievementView({ focusUuid, courseRegId }: AchievementViewProps) {
27+
function AchievementView({ focusUuid, userState }: AchievementViewProps) {
28+
const assessmentId = !Number.isNaN(+focusUuid) && +focusUuid !== 0 ? +focusUuid : undefined;
29+
let courseRegId: number | undefined;
30+
31+
if (userState) {
32+
const [selectedUser] = userState!;
33+
courseRegId = selectedUser?.courseRegId;
34+
}
35+
const userCrid = useTypedSelector(store => store.session.courseRegId);
36+
const isAdminView: boolean = courseRegId !== undefined && courseRegId !== userCrid;
37+
2638
const dispatch = useDispatch();
2739
useEffect(() => {
28-
if (!Number.isNaN(+focusUuid) && +focusUuid !== 0) {
29-
dispatch({ type: FETCH_ASSESSMENT, payload: +focusUuid });
30-
dispatch({ type: FETCH_ASSESSMENT_OVERVIEWS });
40+
dispatch({ type: FETCH_ASSESSMENT_OVERVIEWS });
41+
if (!assessmentId) {
42+
return;
3143
}
32-
}, [focusUuid, courseRegId, dispatch]);
44+
if (isAdminView) {
45+
// Fetch selected user's assessment from admin route
46+
dispatch({ type: FETCH_ASSESSMENT_ADMIN, payload: { assessmentId, courseRegId } });
47+
} else {
48+
// If user is student, fetch assessment details from assessment route instead, as seen below
49+
dispatch({ type: FETCH_ASSESSMENT, payload: assessmentId });
50+
}
51+
}, [dispatch, assessmentId, courseRegId, isAdminView]);
3352

3453
const inferencer = useContext(AchievementContext);
35-
const courseId = useTypedSelector(store => store.session.courseId);
36-
3754
const assessments = useTypedSelector(store => store.session.assessments);
55+
const selectedAssessment: Assessment | undefined = assessments.get(assessmentId!);
3856
const allAssessmentConfigs = useTypedSelector(store => store.session.assessmentOverviews) ?? [];
39-
const selectedAssessment: Assessment | undefined = assessments.get(+focusUuid);
40-
const selectedAssessmentConfig = allAssessmentConfigs.find(config => config.id === +focusUuid);
57+
const selectedAssessmentConfig = allAssessmentConfigs.find(config => config.id === assessmentId);
4158

4259
if (focusUuid === '') {
4360
return (
@@ -47,7 +64,6 @@ function AchievementView({ focusUuid, courseRegId }: AchievementViewProps) {
4764
</div>
4865
);
4966
}
50-
5167
const achievement = inferencer.getAchievement(focusUuid);
5268
const { deadline, title, view } = achievement;
5369
const { coverImage, completionText, description } = view;
@@ -82,7 +98,9 @@ function AchievementView({ focusUuid, courseRegId }: AchievementViewProps) {
8298
selectedAssessment &&
8399
selectedAssessmentConfig &&
84100
selectedAssessmentConfig.isManuallyGraded && (
85-
<AchievementCommentCard courseId={courseId} assessment={selectedAssessment} />
101+
// TODO: showToQuestion is currently used to disable the goto question button for admins,
102+
// as it has not been integrated with the grading view yet
103+
<AchievementCommentCard assessment={selectedAssessment} showToQuestion={!isAdminView} />
86104
)}
87105

88106
{goals.length > 0 && (

src/commons/application/actions/SessionActions.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ import {
2222
DELETE_USER_COURSE_REGISTRATION,
2323
FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS,
2424
FETCH_ASSESSMENT,
25+
FETCH_ASSESSMENT_ADMIN,
2526
FETCH_ASSESSMENT_CONFIGS,
2627
FETCH_ASSESSMENT_OVERVIEWS,
2728
FETCH_AUTH,
2829
FETCH_COURSE_CONFIG,
2930
FETCH_GRADING,
3031
FETCH_GRADING_OVERVIEWS,
3132
FETCH_NOTIFICATIONS,
33+
FETCH_TOTAL_XP,
34+
FETCH_TOTAL_XP_ADMIN,
3235
FETCH_USER_AND_COURSE,
33-
GET_TOTAL_XP,
3436
LOGIN,
3537
LOGIN_GITHUB,
3638
LOGOUT_GITHUB,
@@ -77,11 +79,16 @@ export const fetchUserAndCourse = () => action(FETCH_USER_AND_COURSE);
7779

7880
export const fetchCourseConfig = () => action(FETCH_COURSE_CONFIG);
7981

80-
export const fetchAssessment = (id: number) => action(FETCH_ASSESSMENT, id);
82+
export const fetchAssessment = (assessmentId: number) => action(FETCH_ASSESSMENT, assessmentId);
83+
84+
export const fetchAssessmentAdmin = (assessmentId: number, courseRegId: number) =>
85+
action(FETCH_ASSESSMENT_ADMIN, { assessmentId, courseRegId });
8186

8287
export const fetchAssessmentOverviews = () => action(FETCH_ASSESSMENT_OVERVIEWS);
8388

84-
export const getTotalXp = () => action(GET_TOTAL_XP);
89+
export const fetchTotalXp = () => action(FETCH_TOTAL_XP);
90+
91+
export const fetchTotalXpAdmin = (courseRegId: number) => action(FETCH_TOTAL_XP_ADMIN, courseRegId);
8592

8693
export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, submissionId);
8794

src/commons/application/types/SessionTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ export const FETCH_AUTH = 'FETCH_AUTH';
1616
export const FETCH_USER_AND_COURSE = 'FETCH_USER_AND_COURSE';
1717
export const FETCH_COURSE_CONFIG = 'FETCH_COURSE_CONFIG';
1818
export const FETCH_ASSESSMENT = 'FETCH_ASSESSMENT';
19+
export const FETCH_ASSESSMENT_ADMIN = 'FETCH_ASSESSMENT_ADMIN';
1920
export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS';
20-
export const GET_TOTAL_XP = 'GET_TOTAL_XP';
21+
export const FETCH_TOTAL_XP = 'FETCH_TOTAL_XP';
22+
export const FETCH_TOTAL_XP_ADMIN = 'FETCH_TOTAL_XP_ADMIN';
2123
export const FETCH_GRADING = 'FETCH_GRADING';
2224
export const FETCH_GRADING_OVERVIEWS = 'FETCH_GRADING_OVERVIEWS';
2325
export const LOGIN = 'LOGIN';

src/commons/profile/ProfileContainer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
22
import { bindActionCreators, Dispatch } from 'redux';
33

4-
import { fetchAssessmentOverviews, getTotalXp } from '../application/actions/SessionActions';
4+
import { fetchAssessmentOverviews, fetchTotalXp } from '../application/actions/SessionActions';
55
import { OverallState } from '../application/ApplicationTypes';
66
import Profile, { DispatchProps, StateProps } from './Profile';
77

@@ -18,7 +18,7 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, {}> = (dispatch: Dis
1818
bindActionCreators(
1919
{
2020
handleAssessmentOverviewFetch: fetchAssessmentOverviews,
21-
handleTotalXpFetch: getTotalXp
21+
handleTotalXpFetch: fetchTotalXp
2222
},
2323
dispatch
2424
);

src/commons/sagas/BackendSaga.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@ import {
4646
DELETE_USER_COURSE_REGISTRATION,
4747
FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS,
4848
FETCH_ASSESSMENT,
49+
FETCH_ASSESSMENT_ADMIN,
4950
FETCH_ASSESSMENT_CONFIGS,
5051
FETCH_AUTH,
5152
FETCH_COURSE_CONFIG,
5253
FETCH_GRADING,
5354
FETCH_GRADING_OVERVIEWS,
5455
FETCH_NOTIFICATIONS,
56+
FETCH_TOTAL_XP,
57+
FETCH_TOTAL_XP_ADMIN,
5558
FETCH_USER_AND_COURSE,
56-
GET_TOTAL_XP,
5759
REAUTOGRADE_ANSWER,
5860
REAUTOGRADE_SUBMISSION,
5961
SUBMIT_ANSWER,
@@ -235,7 +237,7 @@ function* BackendSaga(): SagaIterator {
235237
}
236238
});
237239

238-
yield takeEvery(GET_TOTAL_XP, function* () {
240+
yield takeEvery(FETCH_TOTAL_XP, function* () {
239241
const tokens: Tokens = yield selectTokens();
240242

241243
const res: { totalXp: number } = yield call(getTotalXp, tokens);
@@ -244,17 +246,50 @@ function* BackendSaga(): SagaIterator {
244246
}
245247
});
246248

249+
yield takeEvery(
250+
FETCH_TOTAL_XP_ADMIN,
251+
function* (action: ReturnType<typeof actions.fetchTotalXpAdmin>) {
252+
const tokens: Tokens = yield selectTokens();
253+
254+
const courseRegId = action.payload;
255+
256+
const res: { totalXp: number } = yield call(getTotalXp, tokens, courseRegId);
257+
if (res) {
258+
yield put(actions.updateTotalXp(res.totalXp));
259+
}
260+
}
261+
);
262+
247263
yield takeEvery(FETCH_ASSESSMENT, function* (action: ReturnType<typeof actions.fetchAssessment>) {
248264
const tokens: Tokens = yield selectTokens();
249265

250-
const id = action.payload;
266+
const assessmentId = action.payload;
251267

252-
const assessment: Assessment | null = yield call(getAssessment, id, tokens);
268+
const assessment: Assessment | null = yield call(getAssessment, assessmentId, tokens);
253269
if (assessment) {
254270
yield put(actions.updateAssessment(assessment));
255271
}
256272
});
257273

274+
yield takeEvery(
275+
FETCH_ASSESSMENT_ADMIN,
276+
function* (action: ReturnType<typeof actions.fetchAssessmentAdmin>) {
277+
const tokens: Tokens = yield selectTokens();
278+
279+
const { assessmentId, courseRegId } = action.payload;
280+
281+
const assessment: Assessment | null = yield call(
282+
getAssessment,
283+
assessmentId,
284+
tokens,
285+
courseRegId
286+
);
287+
if (assessment) {
288+
yield put(actions.updateAssessment(assessment));
289+
}
290+
}
291+
);
292+
258293
yield takeEvery(SUBMIT_ANSWER, function* (action: ReturnType<typeof actions.submitAnswer>): any {
259294
const tokens: Tokens = yield selectTokens();
260295
const questionId = action.payload.id;

src/commons/sagas/RequestsSaga.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -468,11 +468,22 @@ export const getAssessmentOverviews = async (
468468
/**
469469
* GET /courses/{courseId}/user/total_xp
470470
*/
471-
export const getTotalXp = async (tokens: Tokens): Promise<number | null> => {
472-
const resp = await request(`${courseId()}/user/total_xp`, 'GET', {
473-
...tokens,
474-
shouldRefresh: true
475-
});
471+
export const getTotalXp = async (tokens: Tokens, courseRegId?: number): Promise<number | null> => {
472+
let resp;
473+
if (courseRegId !== undefined) {
474+
// If courseRegId is provided, get the total XP of a specific student
475+
resp = await request(`${courseId()}/admin/users/${courseRegId}/total_xp`, 'GET', {
476+
...tokens,
477+
shouldRefresh: true
478+
});
479+
} else {
480+
// Otherwise, get the total XP of the current user
481+
resp = await request(`${courseId()}/user/total_xp`, 'GET', {
482+
...tokens,
483+
shouldRefresh: true
484+
});
485+
}
486+
476487
if (!resp || !resp.ok) {
477488
return null; // invalid accessToken _and_ refreshToken
478489
}
@@ -514,12 +525,30 @@ export const getUserAssessmentOverviews = async (
514525
* Note: if assessment is password-protected, a corresponding unlock request will be sent to
515526
* POST /courses/{courseId}/assessments/{assessmentId}/unlock
516527
*/
517-
export const getAssessment = async (id: number, tokens: Tokens): Promise<Assessment | null> => {
518-
let resp = await request(`${courseId()}/assessments/${id}`, 'GET', {
519-
...tokens,
520-
shouldAutoLogout: false,
521-
shouldRefresh: true
522-
});
528+
export const getAssessment = async (
529+
assessmentId: number,
530+
tokens: Tokens,
531+
courseRegId?: number
532+
): Promise<Assessment | null> => {
533+
let resp;
534+
if (courseRegId !== undefined) {
535+
// If courseRegId is provided, we are getting the assessment for another user as an admin
536+
resp = await request(
537+
`${courseId()}/admin/users/${courseRegId}/assessments/${assessmentId}`,
538+
'GET',
539+
{
540+
...tokens,
541+
shouldRefresh: true
542+
}
543+
);
544+
} else {
545+
// Otherwise, we are getting the assessment for the current user
546+
resp = await request(`${courseId()}/assessments/${assessmentId}`, 'GET', {
547+
...tokens,
548+
shouldAutoLogout: false,
549+
shouldRefresh: true
550+
});
551+
}
523552

524553
// Attempt to load password-protected assessment
525554
while (resp && resp.status === 403) {
@@ -530,7 +559,7 @@ export const getAssessment = async (id: number, tokens: Tokens): Promise<Assessm
530559
return null;
531560
}
532561

533-
resp = await request(`${courseId()}/assessments/${id}/unlock`, 'POST', {
562+
resp = await request(`${courseId()}/assessments/${assessmentId}/unlock`, 'POST', {
534563
...tokens,
535564
body: {
536565
password: input

0 commit comments

Comments
 (0)