richie-education 2.25.0-b2.dev142 → 2.25.0-b2.dev145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/api/joanie.ts CHANGED
@@ -28,7 +28,9 @@ export async function getFileFromResponse(response: Response): Promise<File> {
28
28
  const dispositionHeader = response.headers.get('Content-Disposition');
29
29
  const matches = dispositionHeader?.match(filenameRegex);
30
30
 
31
- return new File([await response.blob()], matches ? matches[1] : '');
31
+ return new File([await response.blob()], matches ? matches[1] : '', {
32
+ type: response.headers.get('Content-Type') || '',
33
+ });
32
34
  }
33
35
 
34
36
  export function getResponseBody(response: Response) {
@@ -98,12 +98,20 @@ export interface CertificateDefinition {
98
98
  description: string;
99
99
  }
100
100
 
101
- export interface Certificate {
101
+ export type Certificate = {
102
102
  id: string;
103
103
  issued_on: string;
104
104
  certificate_definition: CertificateDefinition;
105
- order: NestedCertificateOrder | NestedCredentialOrder;
106
- }
105
+ } & (
106
+ | {
107
+ order: NestedCertificateOrder | NestedCredentialOrder;
108
+ enrollment: null;
109
+ }
110
+ | {
111
+ enrollment: EnrollmentLight;
112
+ order: null;
113
+ }
114
+ );
107
115
 
108
116
  // - Organization
109
117
  export interface OrganizationLight {
@@ -0,0 +1,47 @@
1
+ import {
2
+ CertificateFactory,
3
+ CertificateOrderFactory,
4
+ CourseLightFactory,
5
+ CourseRunFactory,
6
+ CredentialOrderFactory,
7
+ EnrollmentLightFactory,
8
+ } from 'utils/test/factories/joanie';
9
+ import { CertificateHelper } from '.';
10
+
11
+ describe('CertificateHelper', () => {
12
+ it.each([undefined, null])(
13
+ 'should return undefined if the certificate is not defined',
14
+ (emptyValue) => {
15
+ expect(CertificateHelper.getCourse(emptyValue)).toBeUndefined();
16
+ },
17
+ );
18
+
19
+ it.each([
20
+ CertificateFactory({
21
+ enrollment: EnrollmentLightFactory({
22
+ course_run: CourseRunFactory({
23
+ course: CourseLightFactory({ title: 'Course 1' }).one(),
24
+ }).one(),
25
+ }).one(),
26
+ order: null,
27
+ }).one(),
28
+ CertificateFactory({
29
+ enrollment: null,
30
+ order: CredentialOrderFactory({
31
+ course: CourseLightFactory({ title: 'Course 1' }).one(),
32
+ }).one(),
33
+ }).one(),
34
+ CertificateFactory({
35
+ enrollment: null,
36
+ order: CertificateOrderFactory({
37
+ enrollment: EnrollmentLightFactory({
38
+ course_run: CourseRunFactory({
39
+ course: CourseLightFactory({ title: 'Course 1' }).one(),
40
+ }).one(),
41
+ }).one(),
42
+ }).one(),
43
+ }).one(),
44
+ ])('should return the course from the certificate linked to ', (certificate) => {
45
+ expect(CertificateHelper.getCourse(certificate)?.title).toEqual('Course 1');
46
+ });
47
+ });
@@ -0,0 +1,19 @@
1
+ import { Certificate } from 'types/Joanie';
2
+ import { Nullable } from 'types/utils';
3
+
4
+ export class CertificateHelper {
5
+ /**
6
+ * Get the course from a Certificate according to its type.
7
+ * Indeed, a Certificate can be linked to an Order or an Enrollment.
8
+ */
9
+ static getCourse(certificate?: Nullable<Certificate>) {
10
+ if (!certificate) return undefined;
11
+
12
+ if (certificate.order) {
13
+ if (certificate.order.course) return certificate.order.course;
14
+ else return certificate.order.enrollment.course_run.course;
15
+ }
16
+
17
+ return certificate.enrollment.course_run.course;
18
+ }
19
+ }
@@ -184,6 +184,7 @@ export const CertificateFactory = factory((): Certificate => {
184
184
  id: faker.string.uuid(),
185
185
  certificate_definition: CertificationDefinitionFactory().one(),
186
186
  order: NestedCredentialOrderFactory().one(),
187
+ enrollment: null,
187
188
  issued_on: faker.date.past().toISOString(),
188
189
  };
189
190
  });
@@ -12,6 +12,7 @@ import { DashboardItemCertificate } from 'widgets/Dashboard/components/Dashboard
12
12
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
13
13
  import {
14
14
  CertificateFactory,
15
+ EnrollmentLightFactory,
15
16
  NestedCertificateOrderFactory,
16
17
  NestedCredentialOrderFactory,
17
18
  } from 'utils/test/factories/joanie';
@@ -27,14 +28,27 @@ jest.mock('utils/context', () => ({
27
28
 
28
29
  describe.each([
29
30
  {
30
- label: 'link to a credential order',
31
- OrderFactory: NestedCredentialOrderFactory,
31
+ // Link to a credential order
32
+ overrideFactory: () => ({
33
+ order: NestedCredentialOrderFactory().one(),
34
+ enrollment: null,
35
+ }),
32
36
  },
33
37
  {
34
- label: 'link to a certificate order',
35
- OrderFactory: NestedCertificateOrderFactory,
38
+ // Link to a certificate order
39
+ overrideFactory: () => ({
40
+ order: NestedCertificateOrderFactory().one(),
41
+ enrollment: null,
42
+ }),
36
43
  },
37
- ])('<DashboardCertificate/> $label', ({ label, OrderFactory }) => {
44
+ {
45
+ // Link to an enrollment
46
+ overrideFactory: () => ({
47
+ order: null,
48
+ enrollment: EnrollmentLightFactory().one(),
49
+ }),
50
+ },
51
+ ])('<DashboardCertificate/> $label', ({ overrideFactory }) => {
38
52
  const Wrapper = ({ children }: PropsWithChildren) => {
39
53
  return (
40
54
  <QueryClientProvider client={createTestQueryClient({ user: true })}>
@@ -66,21 +80,24 @@ describe.each([
66
80
  });
67
81
 
68
82
  it('displays a certificate', async () => {
69
- const certificate: Certificate = CertificateFactory({
70
- order: OrderFactory().one(),
71
- }).one();
83
+ const certificate: Certificate = CertificateFactory(overrideFactory()).one();
72
84
  render(
73
85
  <DashboardItemCertificate certificate={certificate} productType={ProductType.CREDENTIAL} />,
74
86
  { wrapper: Wrapper },
75
87
  );
76
88
 
77
89
  await waitFor(() => screen.getByText(certificate.certificate_definition.title));
78
- const orderCourse =
79
- label === 'link to a credential order'
80
- ? certificate.order.course!
81
- : certificate.order.enrollment!.course_run.course;
82
90
 
83
- screen.getByText(orderCourse.title);
91
+ let course;
92
+ if (certificate.enrollment) {
93
+ course = certificate.enrollment.course_run.course;
94
+ } else if (certificate.order!.course) {
95
+ course = certificate.order!.course;
96
+ } else {
97
+ course = certificate.order!.enrollment.course_run.course;
98
+ }
99
+
100
+ screen.getByText(course.title);
84
101
  screen.getByText(
85
102
  'Issued on ' +
86
103
  new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(new Date(certificate.issued_on)),
@@ -88,9 +105,7 @@ describe.each([
88
105
  });
89
106
 
90
107
  it('downloads the certificate', async () => {
91
- const certificate: Certificate = CertificateFactory({
92
- order: OrderFactory().one(),
93
- }).one();
108
+ const certificate: Certificate = CertificateFactory(overrideFactory()).one();
94
109
 
95
110
  fetchMock.get(`https://joanie.test/api/v1.0/certificates/${certificate.id}/download/`, () => ({
96
111
  status: HttpStatusCode.OK,
@@ -1,3 +1,4 @@
1
+ import { useMemo } from 'react';
1
2
  import { Icon, IconTypeEnum } from 'components/Icon';
2
3
  import { Certificate, CertificateDefinition, CourseLight, ProductType } from 'types/Joanie';
3
4
  import {
@@ -6,6 +7,7 @@ import {
6
7
  } from 'widgets/Dashboard/components/DashboardItem/index';
7
8
  import { Maybe } from 'types/utils';
8
9
  import DownloadCertificateButton from 'components/DownloadCertificateButton';
10
+ import { CertificateHelper } from 'utils/CertificateHelper';
9
11
  import CertificateStatus from '../CertificateStatus';
10
12
 
11
13
  interface DashboardItemCertificateProps {
@@ -20,40 +22,33 @@ export const DashboardItemCertificate = ({
20
22
  productType,
21
23
  mode,
22
24
  }: DashboardItemCertificateProps) => {
23
- if (certificate) {
24
- if (certificateDefinition) {
25
+ const getCertificateDefinition = () => {
26
+ if (certificate && certificateDefinition) {
25
27
  throw new Error('certificate and certificateDefinition are mutually exclusive');
28
+ } else if (!certificate && !certificateDefinition) {
29
+ throw new Error('certificate or certificateDefinition is required');
26
30
  }
27
- certificateDefinition = certificate.certificate_definition;
28
- } else if (certificateDefinition) {
29
- if (certificate) {
30
- throw new Error('certificate and certificateDefinition are mutually exclusive');
31
- }
32
- } else {
33
- throw new Error('certificate or certificateDefinition is required');
34
- }
35
31
 
36
- let course: Maybe<CourseLight>;
37
- if (certificate) {
38
- if (certificate.order?.course) {
39
- course = certificate.order.course;
40
- } else {
41
- course = certificate.order.enrollment.course_run.course;
42
- }
43
- }
32
+ return certificate ? certificate.certificate_definition : certificateDefinition;
33
+ };
34
+
35
+ const course: Maybe<CourseLight> = useMemo(
36
+ () => CertificateHelper.getCourse(certificate),
37
+ [certificate],
38
+ );
39
+ const definition = useMemo(getCertificateDefinition, [certificate]);
44
40
 
45
41
  return (
46
42
  <DashboardItem
47
43
  mode={mode}
48
44
  title={course?.title ?? ''}
49
45
  code={'Ref. ' + (course?.code ?? '')}
50
- imageUrl="https://d29emq8to944i.cloudfront.net/cba69447-b9f7-b4d7-c0d5-4d98b5280a4e/thumbnails/1659356729_1080.jpg"
51
46
  imageFile={course?.cover}
52
47
  footer={
53
48
  <>
54
49
  <div className="dashboard-certificate__body">
55
50
  <Icon name={IconTypeEnum.CERTIFICATE} />
56
- <span>{certificateDefinition!.title}</span>
51
+ <span>{definition!.title}</span>
57
52
  </div>
58
53
  <div className="dashboard-certificate__footer">
59
54
  <span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev142",
3
+ "version": "2.25.0-b2.dev145",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {