Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Classroom Dashboard shared notes widget #2201

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).

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

### Added

- Add a widget to download the available shared notes on the classroom dashboard

### Fixed

- downgrade python social auth to version 4.3.0 (#2197)
Expand Down
1 change: 1 addition & 0 deletions env.d/development.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ DJANGO_LTI_CONFIG_CONTACT_EMAIL=fun.dev@fun-mooc.fr
# BBB server credentials
DJANGO_BBB_ENABLED=False
DJANGO_BBB_API_ENDPOINT=https://example.com/bbb/api
DJANGO_BBB_SHARED_NOTES_RETRIEVE_LINK=https://example.com/bbb
DJANGO_BBB_API_SECRET=BbbSecret
# BBB callback through scalelite may use a different secret to sign the sent token
DJANGO_BBB_API_CALLBACK_SECRET=BbbOtherSecret
Expand Down
11 changes: 10 additions & 1 deletion src/backend/marsha/bbb/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from marsha.core.admin import link_field

from .models import Classroom, ClassroomDocument
from .models import Classroom, ClassroomDocument, ClassroomSharedNote


class ClassroomDocumentInline(admin.TabularInline):
Expand Down Expand Up @@ -93,3 +93,12 @@ class ClassroomDocumentAdmin(admin.ModelAdmin):
"classroom__playlist__organization__name",
"filename",
)


@admin.register(ClassroomSharedNote)
class ClassroomSharedNoteAdmin(admin.ModelAdmin):
"""Admin class for the ClassroomSharedNote model"""

verbose_name = _("Classroom shared note")
list_display = ("id", link_field("classroom"), "updated_on")
readonly_fields = ["id", "updated_on"]
2 changes: 2 additions & 0 deletions src/backend/marsha/bbb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
create,
end,
get_recordings,
get_session_shared_note,
join,
process_recordings,
)
Expand Down Expand Up @@ -365,6 +366,7 @@ def service_end(self, request, *args, **kwargs):
Type[rest_framework.response.Response]
HttpResponse with the serialized classroom.
"""
get_session_shared_note(classroom=self.get_object())
try:
response = end(classroom=self.get_object())
status = 200
Expand Down
83 changes: 83 additions & 0 deletions src/backend/marsha/bbb/migrations/0014_classroomsharednote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 4.1.7 on 2023-04-20 15:49

import uuid

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):
dependencies = [
("bbb", "0013_classroom_tools_parameters"),
]

operations = [
migrations.CreateModel(
name="ClassroomSharedNote",
fields=[
(
"deleted",
models.DateTimeField(db_index=True, editable=False, null=True),
),
(
"deleted_by_cascade",
models.BooleanField(default=False, editable=False),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the shared note as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_on",
models.DateTimeField(
default=django.utils.timezone.now,
editable=False,
help_text="date and time at which a shared note was created",
verbose_name="created on",
),
),
(
"updated_on",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a shared note was last updated",
verbose_name="updated on",
),
),
(
"shared_note_url",
models.CharField(
blank=True,
help_text="url of the classroom shared note",
max_length=255,
null=True,
verbose_name="shared note url",
),
),
(
"classroom",
models.ForeignKey(
help_text="classroom to which this shared note belongs",
on_delete=django.db.models.deletion.PROTECT,
related_name="shared notes",
to="bbb.classroom",
verbose_name="classroom shared note",
),
),
],
options={
"verbose_name": "Classroom shared note",
"verbose_name_plural": "Classroom shared notes",
"db_table": "classroom_shared_note",
"ordering": ["-updated_on"],
},
),
]
29 changes: 29 additions & 0 deletions src/backend/marsha/bbb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,32 @@ class Meta:
ordering = ["-created_on"]
verbose_name = _("Classroom recording")
verbose_name_plural = _("Classroom recordings")


class ClassroomSharedNote(BaseModel):
"""Model representing a shared note in a classroom."""

classroom = models.ForeignKey(
to=Classroom,
related_name="shared_notes",
verbose_name=_("classroom shared notes"),
help_text=_("classroom to which this shared note belongs"),
# don't allow hard deleting a classroom if it still contains a recording
on_delete=models.PROTECT,
)

shared_note_url = models.CharField(
max_length=255,
verbose_name=_("shared note url"),
help_text=_("url of the classroom shared note"),
null=True,
blank=True,
)

class Meta:
"""Options for the ``ClassroomSharedNote`` model."""

db_table = "classroom_shared_note"
ordering = ["-updated_on"]
verbose_name = _("Classroom shared note")
verbose_name_plural = _("Classroom shared notes")
44 changes: 43 additions & 1 deletion src/backend/marsha/bbb/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from rest_framework import serializers

from marsha.bbb.models import Classroom, ClassroomDocument, ClassroomRecording
from marsha.bbb.models import (
Classroom,
ClassroomDocument,
ClassroomRecording,
ClassroomSharedNote,
)
from marsha.bbb.utils.bbb_utils import (
ApiMeetingException,
get_meeting_infos,
Expand Down Expand Up @@ -54,6 +59,30 @@ class Meta: # noqa
)


class ClassroomSharedNoteSerializer(ReadOnlyModelSerializer):
"""A serializer to display a ClassroomRecording resource."""

class Meta: # noqa
model = ClassroomSharedNote
fields = (
"id",
"classroom",
"shared_note_url",
"updated_on",
)
read_only_fields = (
"id",
"classroom",
"shared_note_url",
"updated_on",
)

# Make sure classroom UUID is converted to a string during serialization
classroom = serializers.PrimaryKeyRelatedField(
read_only=True, pk_field=serializers.CharField()
)


class ClassroomSerializer(serializers.ModelSerializer):
"""A serializer to display a Classroom resource."""

Expand All @@ -72,6 +101,7 @@ class Meta: # noqa
"starting_at",
"estimated_duration",
"recordings",
"shared_notes",
# specific generated fields
"infos",
"invite_token",
Expand Down Expand Up @@ -100,6 +130,7 @@ class Meta: # noqa
invite_token = serializers.SerializerMethodField()
instructor_token = serializers.SerializerMethodField()
recordings = serializers.SerializerMethodField()
shared_notes = serializers.SerializerMethodField()

def get_infos(self, obj):
"""Meeting infos from BBB server."""
Expand Down Expand Up @@ -137,6 +168,17 @@ def get_recordings(self, obj):
).data
return []

def get_shared_notes(self, obj):
"""Get the shared notes for the classroom.

Only available for admins.
"""
if self.context.get("is_admin", True):
return ClassroomSharedNoteSerializer(
obj.shared_notes.all(), many=True, context=self.context
).data
return []

def update(self, instance, validated_data):
if any(
attribute in validated_data
Expand Down
106 changes: 106 additions & 0 deletions src/backend/marsha/bbb/tests/bbb_utils/test_get_session_shared_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests for the get_recordings service in the ``bbb`` app of the Marsha project."""
from django.test import TestCase, override_settings

import responses

from marsha.bbb.factories import ClassroomFactory, ClassroomRecordingFactory
from marsha.bbb.utils.bbb_utils import get_session_shared_note
from marsha.core.tests.testing_utils import reload_urlconf


@override_settings(
BBB_SHARED_NOTES_RETRIEVE_LINK="https://10.7.7.1/bigbluebutton/sharednotes"
)
@override_settings(BBB_API_ENDPOINT="https://10.7.7.1/bigbluebutton/api")
@override_settings(BBB_API_SECRET="SuperSecret")
@override_settings(BBB_ENABLED=True)
class ClassroomServiceTestCase(TestCase):
"""Test our intentions about the Classroom get_recordings service."""

maxDiff = None

@classmethod
def setUpClass(cls):
super().setUpClass()

# Force URLs reload to use BBB_ENABLED
reload_urlconf()

@responses.activate
def test_get_shared_notes(self):
"""Validate response when multiple recordings exists."""
classroom = ClassroomFactory(
meeting_id="881e8986-9673-11ed-a1eb-0242ac120002", started=True
)

responses.add(
responses.GET,
"https://10.7.7.1/bigbluebutton/api/getMeetingInfo",
match=[
responses.matchers.query_param_matcher(
{
"meetingID": "7a567d67-29d3-4547-96f3-035733a4dfaa",
"checksum": "7f13332ec54e7df0a02d07904746cb5b8b330498",
}
)
],
body="""
<response>
<returncode>SUCCESS</returncode>
<meetingName>random-6256545</meetingName>
<meetingID>random-6256545</meetingID>
<internalMeetingID>ab0da0b4a1f283e94cfefdf32dd761eebd5461ce-1635514947533</internalMeetingID>
<createTime>1635514947533</createTime>
<createDate>Fri Oct 29 13:42:27 UTC 2021</createDate>
<voiceBridge>77581</voiceBridge>
<dialNumber>613-555-1234</dialNumber>
<attendeePW>trac</attendeePW>
<moderatorPW>trusti</moderatorPW>
<running>true</running>
<duration>0</duration>
<hasUserJoined>true</hasUserJoined>
<recording>false</recording>
<hasBeenForciblyEnded>false</hasBeenForciblyEnded>
<startTime>1635514947596</startTime>
<endTime>0</endTime>
<participantCount>1</participantCount>
<listenerCount>0</listenerCount>
<voiceParticipantCount>0</voiceParticipantCount>
<videoCount>0</videoCount>
<maxUsers>0</maxUsers>
<moderatorCount>0</moderatorCount>
<attendees>
<attendee>
<userID>w_2xox6leao03w</userID>
<fullName>User 1907834</fullName>
<role>MODERATOR</role>
<isPresenter>true</isPresenter>
<isListeningOnly>false</isListeningOnly>
<hasJoinedVoice>false</hasJoinedVoice>
<hasVideo>false</hasVideo>
<clientType>HTML5</clientType>
</attendee>
<attendee>
<userID>w_bau7cr7aefju</userID>
<fullName>User 1907834</fullName>
<role>VIEWER</role>
<isPresenter>false</isPresenter>
<isListeningOnly>false</isListeningOnly>
<hasJoinedVoice>false</hasJoinedVoice>
<hasVideo>false</hasVideo>
<clientType>HTML5</clientType>
</attendee>
</attendees>
<metadata>
</metadata>
<isBreakout>false</isBreakout>
</response>
""",
status=200,
)

shared_note_object = get_session_shared_note(classroom.meeting_id)
assert (
shared_note_object.shared_note_url
== "https://10.7.7.1/bigbluebutton/sharednotes/ab0da0b4a1f283e94cfefdf32dd761eebd5461ce-1635514947533/notes.html"
)
Loading