Skip to content

Commit e40451d

Browse files
author
alfredpichard
committed
WIP
1 parent fc93c8c commit e40451d

File tree

11 files changed

+364
-8
lines changed

11 files changed

+364
-8
lines changed

src/backend/marsha/bbb/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_recordings,
2020
join,
2121
process_recordings,
22+
get_session_shared_note,
2223
)
2324
from marsha.core import defaults, permissions as core_permissions
2425
from marsha.core.api import APIViewMixin, ObjectPkMixin, ObjectRelatedMixin
@@ -365,6 +366,11 @@ def service_end(self, request, *args, **kwargs):
365366
Type[rest_framework.response.Response]
366367
HttpResponse with the serialized classroom.
367368
"""
369+
try:
370+
get_session_shared_note(classroom=self.get_object())
371+
except ApiMeetingException as exception:
372+
response = {"message": str(exception)}
373+
status = 400
368374
try:
369375
response = end(classroom=self.get_object())
370376
status = 200
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Generated by Django 4.1.7 on 2023-04-20 15:49
2+
3+
import uuid
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import django.utils.timezone
8+
9+
10+
class Migration(migrations.Migration):
11+
dependencies = [
12+
("bbb", "0013_classroom_tools_parameters"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="ClassroomSharedNote",
18+
fields=[
19+
(
20+
"deleted",
21+
models.DateTimeField(db_index=True, editable=False, null=True),
22+
),
23+
(
24+
"deleted_by_cascade",
25+
models.BooleanField(default=False, editable=False),
26+
),
27+
(
28+
"id",
29+
models.UUIDField(
30+
default=uuid.uuid4,
31+
editable=False,
32+
help_text="primary key for the shared note as UUID",
33+
primary_key=True,
34+
serialize=False,
35+
verbose_name="id",
36+
),
37+
),
38+
(
39+
"created_on",
40+
models.DateTimeField(
41+
default=django.utils.timezone.now,
42+
editable=False,
43+
help_text="date and time at which a shared note was created",
44+
verbose_name="created on",
45+
),
46+
),
47+
(
48+
"updated_on",
49+
models.DateTimeField(
50+
auto_now=True,
51+
help_text="date and time at which a shared note was last updated",
52+
verbose_name="updated on",
53+
),
54+
),
55+
(
56+
"shared_note_url",
57+
models.CharField(
58+
blank=True,
59+
help_text="url of the classroom shared note",
60+
max_length=255,
61+
null=True,
62+
verbose_name="shared note url",
63+
),
64+
),
65+
(
66+
"classroom",
67+
models.ForeignKey(
68+
help_text="classroom to which this shared note belongs",
69+
on_delete=django.db.models.deletion.PROTECT,
70+
related_name="shared notes",
71+
to="bbb.classroom",
72+
verbose_name="classroom shared note",
73+
),
74+
),
75+
],
76+
options={
77+
"verbose_name": "Classroom shared note",
78+
"verbose_name_plural": "Classroom shared notes",
79+
"db_table": "classroom_shared_note",
80+
"ordering": ["-updated_on"],
81+
},
82+
),
83+
]

src/backend/marsha/bbb/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,32 @@ class Meta:
285285
ordering = ["-created_on"]
286286
verbose_name = _("Classroom recording")
287287
verbose_name_plural = _("Classroom recordings")
288+
289+
290+
class ClassroomSharedNote(BaseModel):
291+
"""Model representing a shared note in a classroom."""
292+
293+
classroom = models.ForeignKey(
294+
to=Classroom,
295+
related_name="shared_notes",
296+
verbose_name=_("classroom shared notes"),
297+
help_text=_("classroom to which this shared note belongs"),
298+
# don't allow hard deleting a classroom if it still contains a recording
299+
on_delete=models.PROTECT,
300+
)
301+
302+
shared_note_url = models.CharField(
303+
max_length=255,
304+
verbose_name=_("shared note url"),
305+
help_text=_("url of the classroom shared note"),
306+
null=True,
307+
blank=True,
308+
)
309+
310+
class Meta:
311+
"""Options for the ``ClassroomSharedNote`` model."""
312+
313+
db_table = "classroom_shared_note"
314+
ordering = ["-updated_on"]
315+
verbose_name = _("Classroom shared note")
316+
verbose_name_plural = _("Classroom shared notes")

src/backend/marsha/bbb/serializers.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from rest_framework import serializers
1414

15-
from marsha.bbb.models import Classroom, ClassroomDocument, ClassroomRecording
15+
from marsha.bbb.models import Classroom, ClassroomDocument, ClassroomRecording, ClassroomSharedNote
1616
from marsha.bbb.utils.bbb_utils import (
1717
ApiMeetingException,
1818
get_meeting_infos,
@@ -54,6 +54,28 @@ class Meta: # noqa
5454
)
5555

5656

57+
class ClassroomSharedNoteSerializer(ReadOnlyModelSerializer):
58+
"""A serializer to display a ClassroomRecording resource."""
59+
60+
class Meta: # noqa
61+
model = ClassroomSharedNote
62+
fields = (
63+
"id",
64+
"classroom",
65+
"shared_note_url",
66+
)
67+
read_only_fields = (
68+
"id",
69+
"classroom",
70+
"shared_note_url",
71+
)
72+
73+
# Make sure classroom UUID is converted to a string during serialization
74+
classroom = serializers.PrimaryKeyRelatedField(
75+
read_only=True, pk_field=serializers.CharField()
76+
)
77+
78+
5779
class ClassroomSerializer(serializers.ModelSerializer):
5880
"""A serializer to display a Classroom resource."""
5981

@@ -72,6 +94,7 @@ class Meta: # noqa
7294
"starting_at",
7395
"estimated_duration",
7496
"recordings",
97+
"shared_notes",
7598
# specific generated fields
7699
"infos",
77100
"invite_token",
@@ -100,6 +123,7 @@ class Meta: # noqa
100123
invite_token = serializers.SerializerMethodField()
101124
instructor_token = serializers.SerializerMethodField()
102125
recordings = serializers.SerializerMethodField()
126+
shared_notes = serializers.SerializerMethodField()
103127

104128
def get_infos(self, obj):
105129
"""Meeting infos from BBB server."""
@@ -137,6 +161,17 @@ def get_recordings(self, obj):
137161
).data
138162
return []
139163

164+
def get_shared_notes(self, obj):
165+
"""Get the shared notes for the classroom.
166+
167+
Only available for admins.
168+
"""
169+
if self.context.get("is_admin", True):
170+
return ClassroomSharedNoteSerializer(
171+
obj.shared_notes.all(), many=True, context=self.context
172+
).data
173+
return []
174+
140175
def update(self, instance, validated_data):
141176
if any(
142177
attribute in validated_data

src/backend/marsha/bbb/utils/bbb_utils.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import requests
1111
import xmltodict
1212

13-
from marsha.bbb.models import Classroom, ClassroomRecording
13+
from marsha.bbb.models import Classroom, ClassroomRecording, ClassroomSharedNote
1414
from marsha.core.utils import time_utils
1515

1616

@@ -194,6 +194,37 @@ def get_meeting_infos(classroom: Classroom):
194194
raise exception
195195

196196

197+
def get_session_shared_note(classroom: Classroom):
198+
"""Call BBB API to retrieve shared notes."""
199+
200+
try:
201+
meeting_infos = get_meeting_infos(classroom=classroom)
202+
session_id = meeting_infos["id"]
203+
except ApiMeetingException as exception:
204+
raise exception
205+
206+
url = f"{settings.BBB_API_ENDPOINT}/{session_id}/notes.html"
207+
request = requests.request(
208+
"get",
209+
url,
210+
verify=not settings.DEBUG,
211+
timeout=settings.BBB_API_TIMEOUT,
212+
)
213+
if request.status_code == 200:
214+
classroom_shared_note, created = ClassroomSharedNote.objects.get_or_create(
215+
classroom=classroom, shared_note_url=url
216+
)
217+
logger.info(
218+
"%s shared note uploaded on %s with url %s",
219+
"Created" if created else "Updated",
220+
classroom_shared_note.updated_on.isoformat(),
221+
classroom_shared_note.shared_note_url,
222+
)
223+
return classroom_shared_note
224+
225+
raise ApiMeetingException(request)
226+
227+
197228
def get_recordings(meeting_id: str = None, record_id: str = None):
198229
"""Call BBB API to retrieve recordings."""
199230
parameters = {}

src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Description } from './widgets/Description';
1111
import { Invite } from './widgets/Invite';
1212
import { Recordings } from './widgets/Recordings';
1313
import { Scheduling } from './widgets/Scheduling';
14+
import { SharedNotes } from './widgets/SharedNotes';
1415
import { SupportSharing } from './widgets/SupportSharing';
1516
import { ToolsAndApplications } from './widgets/ToolsAndApplications';
1617

@@ -21,6 +22,7 @@ enum WidgetType {
2122
INVITE = 'INVITE',
2223
SUPPORT_SHARING = 'SUPPORT_SHARING',
2324
RECORDINGS = 'RECORDINGS',
25+
SHARED_NOTES = 'SHARED_NOTES',
2426
}
2527

2628
const widgetLoader: { [key in WidgetType]: WidgetProps } = {
@@ -48,6 +50,10 @@ const widgetLoader: { [key in WidgetType]: WidgetProps } = {
4850
component: <ToolsAndApplications />,
4951
size: WidgetSize.DEFAULT,
5052
},
53+
[WidgetType.SHARED_NOTES]: {
54+
component: <SharedNotes />,
55+
size: WidgetSize.DEFAULT,
56+
},
5157
};
5258

5359
const classroomWidgets: WidgetType[] = [
@@ -57,6 +63,7 @@ const classroomWidgets: WidgetType[] = [
5763
WidgetType.SCHEDULING,
5864
WidgetType.SUPPORT_SHARING,
5965
WidgetType.RECORDINGS,
66+
WidgetType.SHARED_NOTES,
6067
];
6168

6269
export const ClassroomWidgetProvider = () => {

src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/index.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ describe('<Recordings />', () => {
1717
let classroom = classroomMockFactory({ id: '1', started: false });
1818
const classroomRecordings = [
1919
classroomRecordingMockFactory({
20-
started_at: DateTime.fromJSDate(
21-
new Date(2022, 1, 29, 11, 0, 0),
22-
).toISO() as string,
20+
started_at:
21+
DateTime.fromJSDate(new Date(2022, 1, 29, 11, 0, 0)).toISO() ||
22+
undefined,
2323
}),
2424
classroomRecordingMockFactory({
25-
started_at: DateTime.fromJSDate(
26-
new Date(2022, 1, 15, 11, 0, 0),
27-
).toISO() as string,
25+
started_at:
26+
DateTime.fromJSDate(new Date(2022, 1, 15, 11, 0, 0)).toISO() ||
27+
undefined,
2828
}),
2929
];
3030

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { screen } from '@testing-library/react';
2+
import { InfoWidgetModalProvider } from 'lib-components';
3+
import { render } from 'lib-tests';
4+
import { DateTime } from 'luxon';
5+
import React from 'react';
6+
7+
import {
8+
classroomMockFactory,
9+
classroomSharedNoteMockFactory,
10+
} from '@lib-classroom/utils/tests/factories';
11+
import { wrapInClassroom } from '@lib-classroom/utils/wrapInClassroom';
12+
13+
import { SharedNotes } from '.';
14+
15+
describe('<SharedNotes />', () => {
16+
it('displays a list of available shared notes', () => {
17+
let classroom = classroomMockFactory({ id: '1', started: false });
18+
const classroomSharedNotes = [
19+
classroomSharedNoteMockFactory({
20+
updated_on:
21+
DateTime.fromJSDate(new Date(2022, 1, 29, 11, 0, 0)).toISO() ||
22+
undefined,
23+
}),
24+
classroomSharedNoteMockFactory({
25+
updated_on:
26+
DateTime.fromJSDate(new Date(2022, 1, 15, 11, 0, 0)).toISO() ||
27+
undefined,
28+
}),
29+
];
30+
31+
const { rerender } = render(
32+
wrapInClassroom(
33+
<InfoWidgetModalProvider value={null}>
34+
<SharedNotes />,
35+
</InfoWidgetModalProvider>,
36+
classroom,
37+
),
38+
);
39+
40+
expect(screen.getByText('Shared notes')).toBeInTheDocument();
41+
expect(screen.getByText('No shared note available')).toBeInTheDocument();
42+
43+
// simulate updated classroom
44+
classroom = {
45+
...classroom,
46+
shared_notes: classroomSharedNotes,
47+
};
48+
rerender(
49+
wrapInClassroom(
50+
<InfoWidgetModalProvider value={null}>
51+
<SharedNotes />,
52+
</InfoWidgetModalProvider>,
53+
classroom,
54+
),
55+
);
56+
expect(
57+
screen.queryByText('No shared note available'),
58+
).not.toBeInTheDocument();
59+
expect(
60+
screen.getByText('Tuesday, March 1, 2022 - 11:00 AM'),
61+
).toBeInTheDocument();
62+
expect(
63+
screen.getByText('Tuesday, February 15, 2022 - 11:00 AM'),
64+
).toBeInTheDocument();
65+
});
66+
});

0 commit comments

Comments
 (0)