Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
AlfredPichard committed Apr 20, 2023
1 parent fc93c8c commit e40451d
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/backend/marsha/bbb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
get_recordings,
join,
process_recordings,
get_session_shared_note,
)
from marsha.core import defaults, permissions as core_permissions
from marsha.core.api import APIViewMixin, ObjectPkMixin, ObjectRelatedMixin
Expand Down Expand Up @@ -365,6 +366,11 @@ def service_end(self, request, *args, **kwargs):
Type[rest_framework.response.Response]
HttpResponse with the serialized classroom.
"""
try:
get_session_shared_note(classroom=self.get_object())
except ApiMeetingException as exception:
response = {"message": str(exception)}
status = 400
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")
37 changes: 36 additions & 1 deletion src/backend/marsha/bbb/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

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 +54,28 @@ class Meta: # noqa
)


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

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

# 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 +94,7 @@ class Meta: # noqa
"starting_at",
"estimated_duration",
"recordings",
"shared_notes",
# specific generated fields
"infos",
"invite_token",
Expand Down Expand Up @@ -100,6 +123,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 +161,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
33 changes: 32 additions & 1 deletion src/backend/marsha/bbb/utils/bbb_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import requests
import xmltodict

from marsha.bbb.models import Classroom, ClassroomRecording
from marsha.bbb.models import Classroom, ClassroomRecording, ClassroomSharedNote
from marsha.core.utils import time_utils


Expand Down Expand Up @@ -194,6 +194,37 @@ def get_meeting_infos(classroom: Classroom):
raise exception


def get_session_shared_note(classroom: Classroom):
"""Call BBB API to retrieve shared notes."""

try:
meeting_infos = get_meeting_infos(classroom=classroom)
session_id = meeting_infos["id"]
except ApiMeetingException as exception:
raise exception

url = f"{settings.BBB_API_ENDPOINT}/{session_id}/notes.html"
request = requests.request(
"get",
url,
verify=not settings.DEBUG,
timeout=settings.BBB_API_TIMEOUT,
)
if request.status_code == 200:
classroom_shared_note, created = ClassroomSharedNote.objects.get_or_create(
classroom=classroom, shared_note_url=url
)
logger.info(
"%s shared note uploaded on %s with url %s",
"Created" if created else "Updated",
classroom_shared_note.updated_on.isoformat(),
classroom_shared_note.shared_note_url,
)
return classroom_shared_note

raise ApiMeetingException(request)


def get_recordings(meeting_id: str = None, record_id: str = None):
"""Call BBB API to retrieve recordings."""
parameters = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Description } from './widgets/Description';
import { Invite } from './widgets/Invite';
import { Recordings } from './widgets/Recordings';
import { Scheduling } from './widgets/Scheduling';
import { SharedNotes } from './widgets/SharedNotes';
import { SupportSharing } from './widgets/SupportSharing';
import { ToolsAndApplications } from './widgets/ToolsAndApplications';

Expand All @@ -21,6 +22,7 @@ enum WidgetType {
INVITE = 'INVITE',
SUPPORT_SHARING = 'SUPPORT_SHARING',
RECORDINGS = 'RECORDINGS',
SHARED_NOTES = 'SHARED_NOTES',
}

const widgetLoader: { [key in WidgetType]: WidgetProps } = {
Expand Down Expand Up @@ -48,6 +50,10 @@ const widgetLoader: { [key in WidgetType]: WidgetProps } = {
component: <ToolsAndApplications />,
size: WidgetSize.DEFAULT,
},
[WidgetType.SHARED_NOTES]: {
component: <SharedNotes />,
size: WidgetSize.DEFAULT,
},
};

const classroomWidgets: WidgetType[] = [
Expand All @@ -57,6 +63,7 @@ const classroomWidgets: WidgetType[] = [
WidgetType.SCHEDULING,
WidgetType.SUPPORT_SHARING,
WidgetType.RECORDINGS,
WidgetType.SHARED_NOTES,
];

export const ClassroomWidgetProvider = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ describe('<Recordings />', () => {
let classroom = classroomMockFactory({ id: '1', started: false });
const classroomRecordings = [
classroomRecordingMockFactory({
started_at: DateTime.fromJSDate(
new Date(2022, 1, 29, 11, 0, 0),
).toISO() as string,
started_at:
DateTime.fromJSDate(new Date(2022, 1, 29, 11, 0, 0)).toISO() ||
undefined,
}),
classroomRecordingMockFactory({
started_at: DateTime.fromJSDate(
new Date(2022, 1, 15, 11, 0, 0),
).toISO() as string,
started_at:
DateTime.fromJSDate(new Date(2022, 1, 15, 11, 0, 0)).toISO() ||
undefined,
}),
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { screen } from '@testing-library/react';
import { InfoWidgetModalProvider } from 'lib-components';
import { render } from 'lib-tests';
import { DateTime } from 'luxon';
import React from 'react';

import {
classroomMockFactory,
classroomSharedNoteMockFactory,
} from '@lib-classroom/utils/tests/factories';
import { wrapInClassroom } from '@lib-classroom/utils/wrapInClassroom';

import { SharedNotes } from '.';

describe('<SharedNotes />', () => {
it('displays a list of available shared notes', () => {
let classroom = classroomMockFactory({ id: '1', started: false });
const classroomSharedNotes = [
classroomSharedNoteMockFactory({
updated_on:
DateTime.fromJSDate(new Date(2022, 1, 29, 11, 0, 0)).toISO() ||
undefined,
}),
classroomSharedNoteMockFactory({
updated_on:
DateTime.fromJSDate(new Date(2022, 1, 15, 11, 0, 0)).toISO() ||
undefined,
}),
];

const { rerender } = render(
wrapInClassroom(
<InfoWidgetModalProvider value={null}>
<SharedNotes />,
</InfoWidgetModalProvider>,
classroom,
),
);

expect(screen.getByText('Shared notes')).toBeInTheDocument();
expect(screen.getByText('No shared note available')).toBeInTheDocument();

// simulate updated classroom
classroom = {
...classroom,
shared_notes: classroomSharedNotes,
};
rerender(
wrapInClassroom(
<InfoWidgetModalProvider value={null}>
<SharedNotes />,
</InfoWidgetModalProvider>,
classroom,
),
);
expect(
screen.queryByText('No shared note available'),
).not.toBeInTheDocument();
expect(
screen.getByText('Tuesday, March 1, 2022 - 11:00 AM'),
).toBeInTheDocument();
expect(
screen.getByText('Tuesday, February 15, 2022 - 11:00 AM'),
).toBeInTheDocument();
});
});
Loading

0 comments on commit e40451d

Please sign in to comment.