Skip to content

Commit d9496c8

Browse files
author
alfredpichard
committed
✨(backend) add shared note retrieve api endpoint
retrieve shared notes by recreating access link
1 parent a3c6817 commit d9496c8

File tree

10 files changed

+305
-3
lines changed

10 files changed

+305
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
1010

1111
## [4.0.0-beta.20] - 2023-04-20
1212

13+
### Added
14+
15+
- Add a widget to download the available shared notes on the classroom dashboard
16+
1317
### Fixed
1418

1519
- downgrade python social auth to version 4.3.0 (#2197)

env.d/development.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ [email protected]
3131
# BBB server credentials
3232
DJANGO_BBB_ENABLED=False
3333
DJANGO_BBB_API_ENDPOINT=https://example.com/bbb/api
34+
DJANGO_BBB_SHARED_NOTES_RETRIEVE_LINK=https://example.com/bbb
3435
DJANGO_BBB_API_SECRET=BbbSecret
3536
# BBB callback through scalelite may use a different secret to sign the sent token
3637
DJANGO_BBB_API_CALLBACK_SECRET=BbbOtherSecret

src/backend/marsha/bbb/admin.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from marsha.core.admin import link_field
77

8-
from .models import Classroom, ClassroomDocument
8+
from .models import Classroom, ClassroomDocument, ClassroomSharedNote
99

1010

1111
class ClassroomDocumentInline(admin.TabularInline):
@@ -93,3 +93,12 @@ class ClassroomDocumentAdmin(admin.ModelAdmin):
9393
"classroom__playlist__organization__name",
9494
"filename",
9595
)
96+
97+
98+
@admin.register(ClassroomSharedNote)
99+
class ClassroomSharedNoteAdmin(admin.ModelAdmin):
100+
"""Admin class for the ClassroomSharedNote model"""
101+
102+
verbose_name = _("Classroom shared note")
103+
list_display = ("id", link_field("classroom"), "updated_on")
104+
readonly_fields = ["id", "updated_on"]

src/backend/marsha/bbb/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
create,
1818
end,
1919
get_recordings,
20+
get_session_shared_note,
2021
join,
2122
process_recordings,
2223
)
@@ -365,6 +366,7 @@ def service_end(self, request, *args, **kwargs):
365366
Type[rest_framework.response.Response]
366367
HttpResponse with the serialized classroom.
367368
"""
369+
get_session_shared_note(classroom=self.get_object())
368370
try:
369371
response = end(classroom=self.get_object())
370372
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: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212

1313
from rest_framework import serializers
1414

15-
from marsha.bbb.models import Classroom, ClassroomDocument, ClassroomRecording
15+
from marsha.bbb.models import (
16+
Classroom,
17+
ClassroomDocument,
18+
ClassroomRecording,
19+
ClassroomSharedNote,
20+
)
1621
from marsha.bbb.utils.bbb_utils import (
1722
ApiMeetingException,
1823
get_meeting_infos,
@@ -54,6 +59,30 @@ class Meta: # noqa
5459
)
5560

5661

62+
class ClassroomSharedNoteSerializer(ReadOnlyModelSerializer):
63+
"""A serializer to display a ClassroomRecording resource."""
64+
65+
class Meta: # noqa
66+
model = ClassroomSharedNote
67+
fields = (
68+
"id",
69+
"classroom",
70+
"shared_note_url",
71+
"updated_on",
72+
)
73+
read_only_fields = (
74+
"id",
75+
"classroom",
76+
"shared_note_url",
77+
"updated_on",
78+
)
79+
80+
# Make sure classroom UUID is converted to a string during serialization
81+
classroom = serializers.PrimaryKeyRelatedField(
82+
read_only=True, pk_field=serializers.CharField()
83+
)
84+
85+
5786
class ClassroomSerializer(serializers.ModelSerializer):
5887
"""A serializer to display a Classroom resource."""
5988

@@ -72,6 +101,7 @@ class Meta: # noqa
72101
"starting_at",
73102
"estimated_duration",
74103
"recordings",
104+
"shared_notes",
75105
# specific generated fields
76106
"infos",
77107
"invite_token",
@@ -100,6 +130,7 @@ class Meta: # noqa
100130
invite_token = serializers.SerializerMethodField()
101131
instructor_token = serializers.SerializerMethodField()
102132
recordings = serializers.SerializerMethodField()
133+
shared_notes = serializers.SerializerMethodField()
103134

104135
def get_infos(self, obj):
105136
"""Meeting infos from BBB server."""
@@ -137,6 +168,17 @@ def get_recordings(self, obj):
137168
).data
138169
return []
139170

171+
def get_shared_notes(self, obj):
172+
"""Get the shared notes for the classroom.
173+
174+
Only available for admins.
175+
"""
176+
if self.context.get("is_admin", True):
177+
return ClassroomSharedNoteSerializer(
178+
obj.shared_notes.all(), many=True, context=self.context
179+
).data
180+
return []
181+
140182
def update(self, instance, validated_data):
141183
if any(
142184
attribute in validated_data
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Tests for the get_recordings service in the ``bbb`` app of the Marsha project."""
2+
from django.test import TestCase, override_settings
3+
4+
import responses
5+
6+
from marsha.bbb.factories import ClassroomFactory, ClassroomRecordingFactory
7+
from marsha.bbb.utils.bbb_utils import get_session_shared_note
8+
from marsha.core.tests.testing_utils import reload_urlconf
9+
10+
11+
@override_settings(
12+
BBB_SHARED_NOTES_RETRIEVE_LINK="https://10.7.7.1/bigbluebutton/sharednotes"
13+
)
14+
@override_settings(BBB_API_ENDPOINT="https://10.7.7.1/bigbluebutton/api")
15+
@override_settings(BBB_API_SECRET="SuperSecret")
16+
@override_settings(BBB_ENABLED=True)
17+
class ClassroomServiceTestCase(TestCase):
18+
"""Test our intentions about the Classroom get_recordings service."""
19+
20+
maxDiff = None
21+
22+
@classmethod
23+
def setUpClass(cls):
24+
super().setUpClass()
25+
26+
# Force URLs reload to use BBB_ENABLED
27+
reload_urlconf()
28+
29+
@responses.activate
30+
def test_get_shared_notes(self):
31+
"""Validate response when multiple recordings exists."""
32+
classroom = ClassroomFactory(
33+
meeting_id="881e8986-9673-11ed-a1eb-0242ac120002", started=True
34+
)
35+
36+
responses.add(
37+
responses.GET,
38+
"https://10.7.7.1/bigbluebutton/api/getMeetingInfo",
39+
match=[
40+
responses.matchers.query_param_matcher(
41+
{
42+
"meetingID": "7a567d67-29d3-4547-96f3-035733a4dfaa",
43+
"checksum": "7f13332ec54e7df0a02d07904746cb5b8b330498",
44+
}
45+
)
46+
],
47+
body="""
48+
<response>
49+
<returncode>SUCCESS</returncode>
50+
<meetingName>random-6256545</meetingName>
51+
<meetingID>random-6256545</meetingID>
52+
<internalMeetingID>ab0da0b4a1f283e94cfefdf32dd761eebd5461ce-1635514947533</internalMeetingID>
53+
<createTime>1635514947533</createTime>
54+
<createDate>Fri Oct 29 13:42:27 UTC 2021</createDate>
55+
<voiceBridge>77581</voiceBridge>
56+
<dialNumber>613-555-1234</dialNumber>
57+
<attendeePW>trac</attendeePW>
58+
<moderatorPW>trusti</moderatorPW>
59+
<running>true</running>
60+
<duration>0</duration>
61+
<hasUserJoined>true</hasUserJoined>
62+
<recording>false</recording>
63+
<hasBeenForciblyEnded>false</hasBeenForciblyEnded>
64+
<startTime>1635514947596</startTime>
65+
<endTime>0</endTime>
66+
<participantCount>1</participantCount>
67+
<listenerCount>0</listenerCount>
68+
<voiceParticipantCount>0</voiceParticipantCount>
69+
<videoCount>0</videoCount>
70+
<maxUsers>0</maxUsers>
71+
<moderatorCount>0</moderatorCount>
72+
<attendees>
73+
<attendee>
74+
<userID>w_2xox6leao03w</userID>
75+
<fullName>User 1907834</fullName>
76+
<role>MODERATOR</role>
77+
<isPresenter>true</isPresenter>
78+
<isListeningOnly>false</isListeningOnly>
79+
<hasJoinedVoice>false</hasJoinedVoice>
80+
<hasVideo>false</hasVideo>
81+
<clientType>HTML5</clientType>
82+
</attendee>
83+
<attendee>
84+
<userID>w_bau7cr7aefju</userID>
85+
<fullName>User 1907834</fullName>
86+
<role>VIEWER</role>
87+
<isPresenter>false</isPresenter>
88+
<isListeningOnly>false</isListeningOnly>
89+
<hasJoinedVoice>false</hasJoinedVoice>
90+
<hasVideo>false</hasVideo>
91+
<clientType>HTML5</clientType>
92+
</attendee>
93+
</attendees>
94+
<metadata>
95+
</metadata>
96+
<isBreakout>false</isBreakout>
97+
</response>
98+
""",
99+
status=200,
100+
)
101+
102+
shared_note_object = get_session_shared_note(classroom.meeting_id)
103+
assert (
104+
shared_note_object.shared_note_url
105+
== "https://10.7.7.1/bigbluebutton/sharednotes/ab0da0b4a1f283e94cfefdf32dd761eebd5461ce-1635514947533/notes.html"
106+
)

0 commit comments

Comments
 (0)