Skip to content

Commit

Permalink
✨(frontend) add OpenEdx profile page
Browse files Browse the repository at this point in the history
We want to display OpenEdx read-only informations about the user.
To edit theses informations, a link open a new tab with OpenEdx profile
form page.
  • Loading branch information
rlecellier committed Mar 19, 2024
1 parent 0552190 commit b22b510
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/frontend/js/api/lms/dummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { UnknownEnrollment, OpenEdXEnrollment } from 'types';
import { location } from 'utils/indirection/window';
import { CURRENT_JOANIE_DEV_DEMO_USER, RICHIE_USER_TOKEN } from 'settings';
import { base64Decode } from 'utils/base64Parser';
import { Gender, LanguageIsoCode, LevelOfEducation } from 'types/openEdx';
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';

type JWTPayload = {
email: string;
Expand Down Expand Up @@ -89,6 +91,23 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
localStorage.removeItem(RICHIE_DUMMY_IS_LOGGED_IN);
},
accessToken: () => sessionStorage.getItem(RICHIE_USER_TOKEN),
account: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
get: (username: string) => {
return Promise.resolve(
OpenEdxApiProfileFactory({
username: 'j_do',
name: 'John Do',
email: '[email protected]',
country: 'fr',
level_of_education: LevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
gender: Gender.MALE,
year_of_birth: '1971',
language_proficiencies: [{ code: LanguageIsoCode.ENGLISH }],
}).one(),
);
},
},
},
enrollment: {
get: async (url: string, user: Nullable<User>) =>
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/js/api/lms/openedx-fonzie.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { AuthenticationBackend, LMSBackend } from 'types/commonDataProps';
import { APILms } from 'types/api';
import { RICHIE_USER_TOKEN } from 'settings';
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
import { handle } from 'utils/errors/handle';
import { OpenEdxApiProfile } from 'types/openEdx';
import OpenEdxHawthornApiInterface from './openedx-hawthorn';

/**
Expand All @@ -24,6 +27,7 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
routes: {
user: {
me: `${APIConf.endpoint}/api/v1.0/user/me`,
account: `${APIConf.endpoint}/api/user/v1/accounts/:username`,
},
},
};
Expand All @@ -36,6 +40,19 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
accessToken: () => {
return sessionStorage.getItem(RICHIE_USER_TOKEN);
},
account: {
get: (username: string) => {
return fetch(APIOptions.routes.user.account.replace(':username', username), {
credentials: 'include',
}).then((response) => {
if (response.ok) return response.json() as unknown as OpenEdxApiProfile;
if (response.status >= HttpStatusCode.INTERNAL_SERVER_ERROR) {
handle(new Error(`[GET - Account] > ${response.status} - ${response.statusText}`));
}
throw new HttpError(response.status, response.statusText);
});
},
},
},
};
};
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/js/components/Form/Form/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@
flex-direction: column;
}
}

&-footer {
margin-top: $vertical-spacing;
}
}
4 changes: 4 additions & 0 deletions src/frontend/js/components/Form/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ Form.Row = ({ children, className }: PropsWithChildren<{ className?: string }>)
return <div className={c('form-row', className)}>{children}</div>;
};

Form.Footer = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
return <div className={c('form-footer', className)}>{children}</div>;
};

export default Form;
36 changes: 36 additions & 0 deletions src/frontend/js/hooks/useOpenEdxProfile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import { AuthenticationApi } from 'api/authentication';
import { useSessionQuery } from 'utils/react-query/useSessionQuery';
import { OpenEdxProfile, parseOpenEdxApiProfile } from './utils';

interface UseOpenEdxProfileProps {
username: string;
}

const useOpenEdxProfile = ({ username }: UseOpenEdxProfileProps) => {
if (!AuthenticationApi) {
throw new Error('AuthenticationApi is not defined');
}
// TODO(rlecellier): need explaination about differents backend usage.
// does it work not having account implement everywhere ? oO
if (!AuthenticationApi!.account) {
throw new Error('Current AuthenticationApi do not support account request');
}

const intl = useIntl();

// TODO(rlecellier): handle translated error messages
// .catch(() => {
// setError(intl.formatMessage(actualMessages.errorGet));
// }),
const queryFn: () => Promise<OpenEdxProfile> = useCallback(async () => {
const openEdxApiProfile = await AuthenticationApi!.account!.get(username);
return parseOpenEdxApiProfile(intl, openEdxApiProfile);
}, [username]);

const [queryHandler] = useSessionQuery<OpenEdxProfile>(['open-edx-profile'], queryFn);
return queryHandler;
};

export default useOpenEdxProfile;
131 changes: 131 additions & 0 deletions src/frontend/js/hooks/useOpenEdxProfile/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { IntlShape, defineMessages } from 'react-intl';
import countries from 'i18n-iso-countries';
import { Gender, LevelOfEducation, OpenEdxApiProfile } from 'types/openEdx';
import { Maybe } from 'types/utils';

const levelOfEducationMessages = defineMessages<LevelOfEducation>({
[LevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE]: {
id: 'openEdxProfile.levelOfEducation.masterOrProfessionnalDegree',
description:
'Translation for level of education "master or professional degree" in openEdx profile',
defaultMessage: 'Master',
},
[LevelOfEducation.PHD_OR_DOCTORATE]: {
id: 'openEdxProfile.levelOfEducation.phdOrDoctorate',
description: 'Translation for level of education "phd or doctorate" in openEdx profile',
defaultMessage: 'PHD',
},
[LevelOfEducation.BACHELOR_DEGREE]: {
id: 'openEdxProfile.levelOfEducation.bachelorDegree',
description: 'Translation for level of education "bachelor degree" in openEdx profile',
defaultMessage: 'Bachelor degree',
},
[LevelOfEducation.ASSOCIATE_DEGREE]: {
id: 'openEdxProfile.levelOfEducation.associateDegree',
description: 'Translation for level of education "associate degree" in openEdx profile',
defaultMessage: 'Associate degree',
},
[LevelOfEducation.SECONDARY_OR_HIGH_SCHOOL]: {
id: 'openEdxProfile.levelOfEducation.secondaryOrHighSchool',
description: 'Translation for level of education "secondary or high school" in openEdx profile',
defaultMessage: 'Secondary or high school',
},
[LevelOfEducation.JUNIOR_SECONDARY_OR_MIDDLE_SCHOOL]: {
id: 'openEdxProfile.levelOfEducation.juniorSecondaryOrMiddleSchool',
description:
'Translation for level of education "junior secondary or middle school" in openEdx profile',
defaultMessage: 'Junior secondary or middle school',
},
[LevelOfEducation.ELEMENTARY_PRIMARY_SCHOOL]: {
id: 'openEdxProfile.levelOfEducation.elementaryPrimarySchool',
description:
'Translation for level of education "elementary primary school" in openEdx profile',
defaultMessage: 'Elementary primary school',
},
[LevelOfEducation.NONE]: {
id: 'openEdxProfile.levelOfEducation.none',
description: 'Translation for level of education "none" in openEdx profile',
defaultMessage: 'None',
},
[LevelOfEducation.OTHER]: {
id: 'openEdxProfile.levelOfEducation.other',
description: 'Translation for level of education "other" in openEdx profile',
defaultMessage: 'Other',
},
});

const genderMessages = defineMessages<Gender>({
[Gender.MALE]: {
id: 'openEdxProfile.gender.male',
description: 'Translation for gender "male" in openEdx profile',
defaultMessage: 'Male',
},
[Gender.FEMALE]: {
id: 'openEdxProfile.gender.female',
description: 'Translation for gender "female" in openEdx profile',
defaultMessage: 'Female',
},
[Gender.OTHER]: {
id: 'openEdxProfile.gender.other',
description: 'Translation for gender "other" in openEdx profile',
defaultMessage: 'Other',
},
});

export interface OpenEdxProfile {
username: Maybe<string>;
name: Maybe<string>;
country: Maybe<string>;
yearOfBirth: Maybe<string>;
levelOfEducation: Maybe<string>;
email: Maybe<string>;
dateJoined: Maybe<Date>;
gender: Maybe<string>;
// FIXME(rlecellier): openEdx do not return language
// language: Maybe<string>;
favoriteLanguage: Maybe<string>;
}

export const parseOpenEdxApiProfile = (
intl: IntlShape,
data?: OpenEdxApiProfile,
): OpenEdxProfile => {
const [languageCode] = intl.locale.split('-');
const defaultValues: OpenEdxProfile = {
username: undefined,
name: undefined,
email: undefined,
// FIXME(rlecellier): openEdx do not return language
// language: undefined,
country: undefined,
levelOfEducation: undefined,
gender: undefined,
yearOfBirth: undefined,
favoriteLanguage: undefined,
dateJoined: undefined,
};

const parsedData = data
? {
username: data.username,
name: data.name,
email: data.email,
yearOfBirth: data.year_of_birth,
dateJoined: new Date(data.date_joined),
levelOfEducation:
data.level_of_education !== null
? intl.formatMessage(levelOfEducationMessages[data.level_of_education])
: undefined,
gender: data.gender !== null ? intl.formatMessage(genderMessages[data.gender]) : undefined,
country: data.country ? countries.getName(data.country, languageCode) : undefined,
// TODO(rlecellier): get a list of human readable languages
// FIXME(rlecellier): openEdx don't return the language
// language: String(data.language),
favoriteLanguage: data.language_proficiencies.length
? String(data.language_proficiencies[0].code)
: undefined,
}
: defaultValues;

return parsedData;
};
Loading

0 comments on commit b22b510

Please sign in to comment.