From 940fd0aa0cfcec3155a2286536ae1a1100601e7f Mon Sep 17 00:00:00 2001 From: Yoan Le Clanche Date: Mon, 21 Oct 2024 13:35:59 +0200 Subject: [PATCH] Create conference from BBB --- teleforma/forms.py | 1 + ...forma-webclass-create-conferencerecords.py | 38 +++++ .../migrations/0013_auto_20240926_1615.py | 30 ++++ teleforma/models/core.py | 8 + teleforma/static/teleforma/css/teleforma.css | 10 +- .../templates/teleforma/course_detail.html | 4 + .../templates/teleforma/inc/media_list.html | 41 +++-- teleforma/views/core.py | 16 +- teleforma/webclass/admin.py | 2 +- .../0004_alter_webclassrecord_options.py | 17 +++ .../migrations/0005_bbbserver_api_version.py | 18 +++ teleforma/webclass/models.py | 142 +++++++++++++++++- teleforma/webclass/urls.py | 8 +- teleforma/webclass/views.py | 79 +++++++++- 14 files changed, 386 insertions(+), 28 deletions(-) create mode 100644 teleforma/management/commands/teleforma-webclass-create-conferencerecords.py create mode 100644 teleforma/migrations/0013_auto_20240926_1615.py create mode 100644 teleforma/webclass/migrations/0004_alter_webclassrecord_options.py create mode 100644 teleforma/webclass/migrations/0005_bbbserver_api_version.py diff --git a/teleforma/forms.py b/teleforma/forms.py index 7923269f..57193a91 100644 --- a/teleforma/forms.py +++ b/teleforma/forms.py @@ -56,6 +56,7 @@ class BBBConferenceForm(ModelForm): class Meta: model = Conference fields = '__all__' + exclude = ['bbb_server'] class UserProfileForm(ModelForm): class Meta: diff --git a/teleforma/management/commands/teleforma-webclass-create-conferencerecords.py b/teleforma/management/commands/teleforma-webclass-create-conferencerecords.py new file mode 100644 index 00000000..1ce0b219 --- /dev/null +++ b/teleforma/management/commands/teleforma-webclass-create-conferencerecords.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from optparse import make_option +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from ...models.core import Conference +from teleforma.webclass.models import * + + + +class Command(BaseCommand): + help = "Store record id for conferences" + + def handle(self, *args, **options): + bbb = BBBServer.objects.get(pk=2).get_instance() + + missing_conferences = Conference.objects.filter(bbb_record_id__isnull=True, bbb_room_id__isnull=False) + + for conference in missing_conferences: + recordings = bbb.get_recordings(conference.bbb_room_id).get_field('recordings') + if hasattr(recordings, 'get'): + recordings = recordings['recording'] + if type(recordings) is XMLDictNode: + recordings = [recordings] + if recordings: + # get last session id from the same course + sessions = Conference.objects.filter(course=conference.course, course_type=conference.course_type) + last_session = 0 + for session in sessions: + if session.session_as_int > last_session: + last_session = session.session_as_int + + conference.session = last_session + 1 + conference.bbb_record_id = recordings[0]['recordID'] + + print("Add record id %s for conference %s" % (conference.bbb_record_id, conference.id)) + conference.save() \ No newline at end of file diff --git a/teleforma/migrations/0013_auto_20240926_1615.py b/teleforma/migrations/0013_auto_20240926_1615.py new file mode 100644 index 00000000..d5bbda4c --- /dev/null +++ b/teleforma/migrations/0013_auto_20240926_1615.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.25 on 2024-09-26 16:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('webclass', '0003_alter_webclass_session'), + ('teleforma', '0012_auto_20240925_0929'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='bbb_record_id', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Enregistrement BBB'), + ), + migrations.AddField( + model_name='conference', + name='bbb_room_id', + field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='id de la conférence BBB (généré automatiquement)'), + ), + migrations.AddField( + model_name='conference', + name='bbb_server', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='conference_records', to='webclass.bbbserver', verbose_name='Serveur BBB'), + ), + ] diff --git a/teleforma/models/core.py b/teleforma/models/core.py index a20fa082..90e5f604 100755 --- a/teleforma/models/core.py +++ b/teleforma/models/core.py @@ -412,6 +412,14 @@ class Conference(models.Model): notified = models.BooleanField(_('notified'), default=False) notified_live = models.BooleanField("Notifié live", default=False) + # bbb + bbb_room_id = models.CharField( + 'id de la conférence BBB (généré automatiquement)', blank=True, null=True, max_length=255, unique=True) + bbb_record_id = models.CharField("Enregistrement BBB", max_length=255, blank=True, null=True) + # not used for now, but may be handy if we need to optimize performance + bbb_server = models.ForeignKey( + 'webclass.BBBServer', related_name='conference_records', verbose_name='Serveur BBB', on_delete=models.CASCADE, null=True, blank=True) + @property def description(self): return str(self) diff --git a/teleforma/static/teleforma/css/teleforma.css b/teleforma/static/teleforma/css/teleforma.css index 15e037ca..deb06fb9 100644 --- a/teleforma/static/teleforma/css/teleforma.css +++ b/teleforma/static/teleforma/css/teleforma.css @@ -1727,7 +1727,15 @@ form.add_actus #id_text_parent{ padding: 0em; font-weight: bold; font-size: 1.2em; - } + display: flex; + flex-direction: row; + justify-content: space-between; +} +.create-bbb-conference { + font-size: 0.8em; + font-weight: normal; +} + .course_content { background-color: #FFF; diff --git a/teleforma/templates/teleforma/course_detail.html b/teleforma/templates/teleforma/course_detail.html index 10336f93..7c4cfde9 100644 --- a/teleforma/templates/teleforma/course_detail.html +++ b/teleforma/templates/teleforma/course_detail.html @@ -32,8 +32,12 @@
{{ course.title }} - {{ type }}{% if course.description %} - {{ course.description }}{% endif %} + {% if user.is_staff or user.professor.count %}Créer une conférence{% endif %}
+ + + {% if type.name == 'Quiz' %}
{% if course.quiz.all %} diff --git a/teleforma/templates/teleforma/inc/media_list.html b/teleforma/templates/teleforma/inc/media_list.html index 720994b7..9224be4c 100644 --- a/teleforma/templates/teleforma/inc/media_list.html +++ b/teleforma/templates/teleforma/inc/media_list.html @@ -12,22 +12,37 @@ {% for conference in all_conferences %} - {% if conference.video %} + + + {% if conference.video or conference.record %} - {% else %} -
{% trans 'Click here' %}
+ + {% endif %} - {% comment %}
{% trans 'Click here' %}
{% endcomment %} - - {% conference_publication conference as publication %}
- - {% if conference.video.poster_file %} - {% thumbnail conference.video.poster_file "168x96" as im %} -
- {% trans 'Click here' %} -
- {% endthumbnail %} + {% if conference.record %} +
+ + {% if conference.record.preview %} + {% trans 'Click here' %} xxx {{conference.record}} + {% else %} + {% trans 'Click here' %} + {% endif %} + + + + {% if conference.video.poster_file %} + {% thumbnail conference.video.poster_file "168x96" as im %} +
+ {% trans 'Click here' %} +
+ {% endthumbnail %} + {% else %} +
{% trans 'Click here' %}
+ {% endif %} + {% comment %}
{% trans 'Click here' %}
{% endcomment %} +
+
diff --git a/teleforma/views/core.py b/teleforma/views/core.py index 1559d26a..d189e021 100644 --- a/teleforma/views/core.py +++ b/teleforma/views/core.py @@ -91,7 +91,7 @@ from ..models.core import (Conference, Course, CourseType, Department, Document, DocumentType, Media, MediaTranscoded, Organization, Period, Professor, get_user_role) -from ..webclass.models import Webclass, WebclassRecord +from ..webclass.models import Webclass, WebclassRecord, get_records_from_bbb from .pages import get_page_content # def render(request, template, data=None, mimetype=None): @@ -160,11 +160,25 @@ def get_course_conferences(period, course, course_type, status_min=3): course_type=course_type, status__gte=status_min).distinct() + record_ids = set() for conference in cc: # do not include conferences with publication rules if conference.id not in already_added: conferences.append(conference) + + if conference.bbb_record_id: + record_ids.add(conference.bbb_record_id) + conferences = sorted(conferences, key=lambda c:-c.session_as_int) + + if record_ids: + records = get_records_from_bbb(recording_id=','.join(record_ids)) + records_dict = {record['id']: record for record in records} + + # print(records) + for conference in conferences: + if conference.bbb_record_id: + conference.record = records_dict.get(conference.bbb_record_id) return conferences diff --git a/teleforma/webclass/admin.py b/teleforma/webclass/admin.py index 8dc7c4af..46b267e0 100644 --- a/teleforma/webclass/admin.py +++ b/teleforma/webclass/admin.py @@ -6,7 +6,7 @@ from teleforma.webclass.models import (BBBServer, Webclass, WebclassRecord, class BBBServerAdmin(admin.ModelAdmin): model = BBBServer - list_display = ('url', 'api_key') + list_display = ('url', 'api_key', 'api_version') class WebclassSlotInline(admin.StackedInline): model = WebclassSlot diff --git a/teleforma/webclass/migrations/0004_alter_webclassrecord_options.py b/teleforma/webclass/migrations/0004_alter_webclassrecord_options.py new file mode 100644 index 00000000..1183b838 --- /dev/null +++ b/teleforma/webclass/migrations/0004_alter_webclassrecord_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.25 on 2024-09-26 16:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('webclass', '0003_alter_webclass_session'), + ] + + operations = [ + migrations.AlterModelOptions( + name='webclassrecord', + options={'verbose_name': 'enregistrement webclass', 'verbose_name_plural': 'enregistrements webclass'}, + ), + ] diff --git a/teleforma/webclass/migrations/0005_bbbserver_api_version.py b/teleforma/webclass/migrations/0005_bbbserver_api_version.py new file mode 100644 index 00000000..29e1886c --- /dev/null +++ b/teleforma/webclass/migrations/0005_bbbserver_api_version.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-09-30 16:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webclass', '0004_alter_webclassrecord_options'), + ] + + operations = [ + migrations.AddField( + model_name='bbbserver', + name='api_version', + field=models.CharField(default='1', max_length=100, verbose_name='API Version'), + ), + ] diff --git a/teleforma/webclass/models.py b/teleforma/webclass/models.py index 430ae8d7..0ae0791e 100644 --- a/teleforma/webclass/models.py +++ b/teleforma/webclass/models.py @@ -3,6 +3,8 @@ import calendar import datetime from datetime import date, timedelta +import random +import string from unidecode import unidecode import django.db.models as models @@ -48,7 +50,15 @@ def get_records_from_bbb(**kwargs): recordings = [recordings] for recording in recordings: # recording.prettyprint() - url = recording.get('playback', {}).get('format', {}).get('url') + format = {} + if server.api_version == '1': + format = recording.get('playback', {}).get('format', {}) + else: + formats = recording.get('playback', {}).get('format', []) + for format in formats: + if format.get('type') == 'presentation': + break + url = format.get('url') if url: url = str(url) else: @@ -63,7 +73,7 @@ def get_records_from_bbb(**kwargs): 'end': end, 'end_date': datetime.datetime.fromtimestamp(end), 'url': url, - 'preview': str(recording.get('playback', {}).get('format', {}).get('preview', {}).get('images', {}).get('image', [])), + 'preview': str(format.get('preview', {}).get('images', {}).get('image', '')), 'state': str(recording['state']), } if recording['metadata'].get('periodid'): @@ -113,6 +123,7 @@ def get_records(period_id=None, course_id=None, rooms=None, recording_id=None): class BBBServer(models.Model): url = models.CharField("Url du serveur BBB", max_length=100) api_key = models.CharField("API Key", max_length=100) + api_version = models.CharField("API Version", max_length=100, default="1") class Meta(MetaCore): db_table = app_label + '_' + 'bbb_server' @@ -424,19 +435,22 @@ class WebclassRecord(models.Model): class Meta(MetaCore): db_table = app_label + '_' + 'webclass_record' - verbose_name = 'enregistrement' - verbose_name_plural = 'enregistrements' + verbose_name = 'enregistrement webclass' + verbose_name_plural = 'enregistrements webclass' def __str__(self): return "Enregistrement webclass %d" % self.id - @staticmethod - def get_records(period, course): + @classmethod + def get_records(cls, period, course): record_ids = set() + records_mapping = {} # id : category mapping category_mapping = {} - for record in WebclassRecord.objects.filter(period=period, course=course): + # get current class + for record in cls.objects.filter(period=period, course=course): record_ids.add(record.record_id) + records_mapping[record.record_id] = record category_mapping[record.record_id] = record.category if not record_ids: return {} @@ -445,9 +459,121 @@ class WebclassRecord(models.Model): # group records by category categories = {} for record in records: + record['obj'] = records_mapping.get(record['id']) category = category_mapping[record['id']] if category not in categories: categories[category] = [] categories[category].append(record) - return categories + +# class ConferenceRecord(WebclassRecord): + +# room_id = models.CharField( +# 'id de la conférence BBB (généré automatiquement)', blank=True, null=True, max_length=255, unique=True) + +# category = None +# course_type = models.ForeignKey('teleforma.CourseType', verbose_name=_('course type'), on_delete=models.CASCADE) + + +# class Meta(MetaCore): +# db_table = app_label + '_' + 'conference_record' +# verbose_name = 'enregistrement conférence' +# verbose_name_plural = 'enregistrements conférence' + +# def __str__(self): +# return "Enregistrement conférence %d" % self.id + +# @classmethod +# def get_bbb_instance(cls): +# return BBBServer.objects.get(pk=2).get_instance() + + +# @classmethod +# def create_bbb_room(cls, request, period_id, course_id, course_type_id): +# """ create a BBB room and generate meeting id and moderator password """ + +# username = request.user.get_full_name() +# is_professor = len(request.user.professor.all()) >= 1 +# is_staff = request.user.is_staff or request.user.is_superuser +# if not is_professor and not is_staff: +# raise ValueError("User is not a professor or staff") + + +# year = datetime.datetime.now().year +# bbb = cls.get_bbb_instance() + +# # generate password +# # password = User.objects.make_random_password() +# password = "test2" +# # generate random room id +# room_id = "".join(random.choices(string.ascii_letters + string.digits, k=20)) + +# params = { +# 'name': "test", +# 'moderatorPW': password, +# # 'attendeePW': password+"2", +# # 'maxParticipants':self.webclass_max_participants + 1, +# 'welcome': "Pré-Barreau : Bienvenue sur la conférence \"%s\"." % ("xxxx"), +# 'record': True, +# # 'autoStartRecording':True, +# 'muteOnStart': True, +# 'allowModsToUnmuteUsers': True, +# # 'logo':'https://e-learning.crfpa.pre-barreau.com/static/teleforma/images/logo_pb.png', +# 'copyright': "© %d Pré-Barreau" % year, +# # 'guestPolicy':'ALWAYS_ACCEPT' +# 'bannerText': "Pré-Barreau", +# 'bannerColor': "#003768", +# # 'customStyleUrl': site_url+"/static/teleforma/css/bbb.css" +# } +# meta = { +# 'origin': 'ae', +# 'periodid': period_id, +# 'courseid': course_id, +# 'course_type_id': course_type_id, +# 'professorid': request.user.id, +# } +# try: +# result = bbb.create_meeting( +# room_id, params=params, meta=meta) +# except BBBException as e: +# print(e) +# raise +# ConferenceRecord.objects.create( +# room_id=room_id, +# bbb_server=BBBServer.objects.get(pk=2), +# period_id=period_id, +# course_id=course_id, +# course_type_id=course_type_id, +# ) + + +# params = {'userID': request.user.username} +# print(room_id) +# return bbb.get_join_meeting_url(username, room_id, password, params) + + +# @classmethod +# def get_records(cls, period, course, course_type): +# record_ids = set() +# records_mapping = {} +# # id : category mapping +# category_mapping = {} +# # get current class +# for record in cls.objects.filter(period=period, course=course, course_type=course_type, record_id__isnull=False): +# record_ids.add(record.record_id) +# records_mapping[record.record_id] = record +# category_mapping[record.record_id] = record.category +# if not record_ids: +# return {} +# records = get_records_from_bbb(recording_id=','.join(record_ids)) + +# # group records by category +# categories = {} +# for record in records: +# record['obj'] = records_mapping.get(record['id']) +# category = category_mapping[record['id']] +# if category not in categories: +# categories[category] = [] +# categories[category].append(record) +# print(categories) +# return categories \ No newline at end of file diff --git a/teleforma/webclass/urls.py b/teleforma/webclass/urls.py index 7cc6b9cf..c4105ed8 100644 --- a/teleforma/webclass/urls.py +++ b/teleforma/webclass/urls.py @@ -37,7 +37,7 @@ from django.conf.urls import url from ..webclass.views import (WebclassAppointment, WebclassProfessorAppointments, WebclassRecordsFormView, WebclassRecordView, - join_webclass, unregister) + join_webclass, unregister, create_bbb_conference) urlpatterns = [ url(r'^desk/webclass_appointments/(?P.*)$', WebclassAppointment.as_view(), @@ -53,6 +53,8 @@ urlpatterns = [ name="teleforma-webclass-unregister"), url(r'^desk/webclass/(?P.*)/join/$', join_webclass, - name="teleforma-webclass-join") - + name="teleforma-webclass-join"), + url(r'^desk/webclass/(?P.*)/(?P.*)/(?P.*)/create_conference/$', + create_bbb_conference, + name="teleforma-create-bbb-conference") ] diff --git a/teleforma/webclass/views.py b/teleforma/webclass/views.py index 3a4c1788..d7dff615 100644 --- a/teleforma/webclass/views.py +++ b/teleforma/webclass/views.py @@ -7,12 +7,19 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic import FormView, TemplateView, View +from bigbluebutton_api_python.exception import BBBException + from datetime import datetime +import random +import string + +from ..models.core import Conference, Course, Professor from ..decorators import access_required from ..views.core import get_courses, get_periods from ..webclass.forms import WebclassRecordsForm -from ..webclass.models import Webclass, WebclassSlot +from ..webclass.models import BBBServer, Webclass, WebclassSlot +from django.contrib.auth.models import User class WebclassProfessorAppointments(TemplateView): @@ -228,3 +235,73 @@ def unregister(request, pk): "Votre réservation a été annulé.") # redirect to register form return redirect(reverse("teleforma-webclass-appointments", kwargs={'pk':webclass_slot.webclass.id})) + + + +@access_required +def create_bbb_conference(request, period_id, course_id, course_type_id): + """ + Create a BBB conference + """ + + username = request.user.get_full_name() + is_professor = len(request.user.professor.all()) >= 1 + is_staff = request.user.is_staff or request.user.is_superuser + if not is_professor and not is_staff: + raise ValueError("User is not a professor or staff") + course = Course.objects.get(pk=course_id) + + + year = datetime.now().year + bbb = BBBServer.objects.get(pk=2).get_instance() + + # generate password + password = User.objects.make_random_password() + # generate random room id + room_id = "".join(random.choices(string.ascii_letters + string.digits, k=20)) + + params = { + 'name': "Conférence %s" % course.title, + 'moderatorPW': password, + # 'attendeePW': password+"2", + # 'maxParticipants':self.webclass_max_participants + 1, + 'welcome': "Pré-Barreau : Bienvenue sur la conférence \"%s\"." % (course.title), + 'record': False, + # 'autoStartRecording':True, + 'muteOnStart': True, + 'allowModsToUnmuteUsers': True, + # 'logo':'https://e-learning.crfpa.pre-barreau.com/static/teleforma/images/logo_pb.png', + 'copyright': "© %d Pré-Barreau" % year, + # 'guestPolicy':'ALWAYS_ACCEPT' + 'bannerText': "Pré-Barreau", + 'bannerColor': "#003768", + # 'customStyleUrl': site_url+"/static/teleforma/css/bbb.css" + } + meta = { + 'origin': 'ae', + 'periodid': period_id, + 'courseid': course_id, + 'course_type_id': course_type_id, + 'professorid': request.user.id, + } + try: + result = bbb.create_meeting( + room_id, params=params, meta=meta) + except BBBException as e: + print(e) + raise + try: + professor = request.user.professor.get() + except Professor.DoesNotExist: + professor = None + Conference.objects.create( + bbb_room_id=room_id, + bbb_server=BBBServer.objects.get(pk=2), + period_id=period_id, + course_id=course_id, + course_type_id=course_type_id, + professor=professor, + ) + + params = {'userID': request.user.username} + return redirect(bbb.get_join_meeting_url(username, room_id, password, params)) \ No newline at end of file -- 2.39.5