From fdfea86b7089c1634487e00b3ca8465c8f9ff762 Mon Sep 17 00:00:00 2001 From: Yoan Le Clanche Date: Thu, 7 May 2020 14:10:00 +0200 Subject: [PATCH] WIP --- teleforma/bbb_utils.py | 12 ++ .../commands/teleforma-revisions-from-bbb.py | 74 ++++++++++++ teleforma/models/core.py | 113 +++++++++++++----- .../templates/teleforma/course_webclass.html | 32 ++++- .../templates/teleforma/seminar_detail.html | 22 ++-- teleforma/urls.py | 8 ++ teleforma/views/core.py | 50 +++++++- teleforma/views/pro.py | 66 +++++++++- 8 files changed, 314 insertions(+), 63 deletions(-) create mode 100644 teleforma/bbb_utils.py create mode 100644 teleforma/management/commands/teleforma-revisions-from-bbb.py mode change 100644 => 100755 teleforma/models/core.py diff --git a/teleforma/bbb_utils.py b/teleforma/bbb_utils.py new file mode 100644 index 00000000..68d4e9f9 --- /dev/null +++ b/teleforma/bbb_utils.py @@ -0,0 +1,12 @@ +from django.conf import settings +from bigbluebutton_api_python import BigBlueButton +from django.core.urlresolvers import reverse + +bbb = BigBlueButton(settings.BBB_SERVER, settings.BBB_SECRET) + +def register_web_hook(): + if settings.BBB_USE_WEBHOOKS: + hook_url = reverse('webclass_bbb_webhook') + hook_url = settings.TELEFORMA_MASTER_HOST + hook_url + bbb.create_hook(hook_url) + diff --git a/teleforma/management/commands/teleforma-revisions-from-bbb.py b/teleforma/management/commands/teleforma-revisions-from-bbb.py new file mode 100644 index 00000000..994cc476 --- /dev/null +++ b/teleforma/management/commands/teleforma-revisions-from-bbb.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from teleforma.models import * +from teleforma.bbb_utils import bbb +import sys + +import datetime + +def as_list(what): + """ + Ensure something is a list or tuple + """ + if isinstance(what, (list, tuple)): + return what + return [ what ] + +class Command(BaseCommand): + help = "Credit time to students connected to a Big Blue Button webclass" + args = "duration" + + def handle(self, *args, **options): + if len(args) != 1 or not args[0].isdigit(): + print("Syntax: %s %s " % (sys.argv[0], + sys.argv[1])) + sys.exit(1) + duration = int(args[0]) + meetings = bbb.get_meetings() + meetings = as_list(meetings.get_field("meetings")["meeting"]) + + end = datetime.datetime.now() + start = end - datetime.timedelta(seconds = duration) + + print("=== Starting at %s" % end.strftime('%Y-%m-%d %H:%M:%S')) + + done = set() + + for meeting in meetings: + meeting_id = str(meeting['meetingID']) + try: + conf = Conference.objects.get(webclass_id = meeting_id) + seminar = Seminar.objects.get(conference = conf) + except: + print("Warning, can't find Seminar for %s" % meeting_id) + continue + + sem_txt = "%s (%s)" % (seminar, seminar.pk) + attendees = as_list(meeting['attendees']['attendee']) + for attendee in attendees: + user_id = str(attendee['userID']) + key = (meeting_id, user_id) + if key in done: + print("Warning, user %s duplicate for %s" % (user_id, sem_txt)) + continue + done.add(key) + + try: + user = User.objects.get(username = user_id) + except: + print("Warning, can't find user %s for seminar %s" % (user_id, sem_txt)) + continue + user_txt = "%s (%s)" % (user, user.pk) + rev = SeminarRevision.objects.filter(seminar = seminar, + user = user, + date_modified = None) + if rev.count(): + print("User %s already has an open revision on %s" % (user_txt, sem_txt)) + else: + print("Crediting %d seconds to %s on %s" % (duration, user_txt, sem_txt)) + sr = SeminarRevision(seminar = seminar, + user = user, + date = start, + date_modified = end) + sr.save() diff --git a/teleforma/models/core.py b/teleforma/models/core.py old mode 100644 new mode 100755 index 784b9f04..4c531749 --- a/teleforma/models/core.py +++ b/teleforma/models/core.py @@ -62,13 +62,14 @@ from mezzanine.core.models import Displayable from mezzanine.core.managers import DisplayableManager from django.core.urlresolvers import reverse from django.template.defaultfilters import date -from bigbluebutton_api_python import BigBlueButton +from bigbluebutton_api_python.exception import BBBException from django.db.models.signals import pre_save from django.dispatch import receiver +from teleforma.bbb_utils import bbb, register_web_hook + app_label = 'teleforma' -bbb = BigBlueButton(settings.BBB_SERVER, settings.BBB_SECRET) def get_n_choices(n): return [(str(x), str(y)) for x in range(1, n) for y in range(1, n) if x == y] @@ -535,50 +536,98 @@ class WebclassMixin(Model): class Meta(MetaCore): abstract = True - def create_webclass_room(self): - """ create a BBB room and generate meeting id and moderator password """ + def prepare_webclass(self): + """ + generate room id and moderator password + """ if not self.webclass_id and self.webclass: - meeting_id = self.slug + "-" + str(self.period) + meeting_id = self.slug + "-" + self.period password = User.objects.make_random_password() - params = { - 'moderatorPW':password, - # 'maxParticipants':self.webclass_max_participants + 1, - # 'welcome':"Bienvenue sur la conférence \"%s\"." % (str(self.title),), - 'record':True, - 'autoStartRecording':True, - 'muteOnStart':True, - 'allowModsToUnmuteUsers':True, - 'guestPolicy':'ALWAYS_DENY' - } - - try: - result = bbb.create_meeting(meeting_id, params=params) - except BigBlueButton.BBBException: - raise - - meeting_id = result['xml']['meetingID'].decode() self.webclass_id = meeting_id self.webclass_password = password + + def create_webclass_room(self): + """ create a BBB room and generate meeting id and moderator password """ + if self.webclass_id and self.webclass: + try: + # check if meeting already exists + self.get_webclass_info() + except BBBException: + params = { + 'moderatorPW':self.webclass_password, + 'attendeePW':'pwattendee', + # 'maxParticipants':self.webclass_max_participants + 1, + 'welcome':"Bienvenue sur la conférence \"%s\"." % (str(self.title),), + 'record':True, + # 'autoStartRecording':True, + 'muteOnStart':True, + 'allowModsToUnmuteUsers':True, + # 'guestPolicy':'ALWAYS_ACCEPT' + } + + try: + result = bbb.create_meeting(self.webclass_id, params=params) + register_web_hook() + except BBBException as e: + print(e) + raise + - def get_join_webclass_url(self, request): + def get_join_webclass_url(self, user): """ Get url to BBB meeting. If user are professor or staff, provide the url with the moderator password """ - username = request.user.get_full_name() - is_professor = len(request.user.professor.all()) >= 1 - is_staff = request.user.is_staff - password = (is_professor or is_staff) and self.webclass_password or "" - return bbb.get_join_meeting_url(username, self.webclass_id, password) + self.create_webclass_room() + username = user.get_full_name() + is_professor = len(user.professor.all()) >= 1 + is_staff = user.is_staff or user.is_superuser + password = 'pwattendee' + if is_professor or is_staff: + password = self.webclass_password + params = {'userID': user.username} + print(bbb.get_join_meeting_url(username, self.webclass_id, password, params)) + return bbb.get_join_meeting_url(username, self.webclass_id, password, params) def is_webclass_running(self): """ Is webclass currently running ? """ + # print(self.get_webclass_info()) return bbb.is_meeting_running(self.webclass_id).get_field('running').decode() == 'true' or False - # def get_webclass_info(self): - # """ """ - # return bbb.get_meeting_info(self.webclass_id) + def get_webclass_info(self): + """ """ + print(self.webclass_id) + print(bbb.get_meeting_info(self.webclass_id)) + return bbb.get_meeting_info(self.webclass_id) + + def get_record(self): + """ get longest published record for the current conference """ + all_records = [] + for recording in bbb.get_recordings(self.webclass_id).get_field('recordings')['recording']: + recording.prettyprint() + url = recording.get('playback', {}).get('format', {}).get('url') + if url: + url = url.decode() + data = { + 'start': int(recording['startTime'].decode()), + 'end': int(recording['endTime'].decode()), + 'url': url, + 'state': recording['state'].decode(), + } + data['duration'] = data['end'] - data['start'] + all_records.append(data) + + print('all_records') + print(all_records) + if not all_records: + return None + all_records = sorted(all_records, key=lambda record:-record['duration']) + longest_record = all_records[0] + if not longest_record['url'] or longest_record['state'] != 'published': + return None + return longest_record + @@ -728,7 +777,7 @@ class Conference(Displayable, WebclassMixin): @receiver(pre_save, sender=Conference) def create_webclass_room(sender, **kwargs): instance = kwargs['instance'] - instance.create_webclass_room() + instance.prepare_webclass() class NamePaginator(object): """Pagination for string-based objects""" diff --git a/teleforma/templates/teleforma/course_webclass.html b/teleforma/templates/teleforma/course_webclass.html index a154f537..8107b636 100644 --- a/teleforma/templates/teleforma/course_webclass.html +++ b/teleforma/templates/teleforma/course_webclass.html @@ -9,13 +9,13 @@ diff --git a/teleforma/urls.py b/teleforma/urls.py index cb065723..605bb788 100644 --- a/teleforma/urls.py +++ b/teleforma/urls.py @@ -96,6 +96,10 @@ urlpatterns = patterns('', name="teleforma-conference-audio"), url(r'^desk/conference_record/$', ConferenceRecordView.as_view(), name="teleforma-conference-record"), + url(r'^desk/conferences/(?P.*)/join/$', + join_webclass, + name="teleforma-conference-join"), + # Questions url(r'^desk/seminars/(?P.*)/questions/(?P.*)/$', AnswerView.as_view(), @@ -166,4 +170,8 @@ urlpatterns = patterns('', url(r'^desk/test/(?P.*)/$', AnswerDetailViewTest.as_view(), name="test"), + # Webclass hook + url(r'^webclass_bbb_webhook/', webclass_bbb_webhook, + name="webclass_bbb_webhook"), + ) diff --git a/teleforma/views/core.py b/teleforma/views/core.py index 0f4ec004..b5d2d571 100644 --- a/teleforma/views/core.py +++ b/teleforma/views/core.py @@ -34,6 +34,7 @@ import mimetypes import datetime +from datetime import timedelta import random import urllib import urllib2 @@ -446,23 +447,39 @@ class ConferenceView(DetailView): def get_context_data(self, **kwargs): context = super(ConferenceView, self).get_context_data(**kwargs) conference = self.get_object() - if conference.webclass: + if conference.webclass: context['is_webclass_running'] = conference.is_webclass_running() - context['webclass_url'] = conference.get_join_webclass_url(self.request) context['show_record'] = True context['seminar'] = conference.seminar.all()[0] - + context['record'] = None + webclass_status = "" + + now = datetime.datetime.now() + if conference.date_begin - timedelta(hours=1) > now: + # conference not yet started + webclass_status = "future" + elif conference.date_end < now: + # conference expired + webclass_status = "past" + context['record'] = conference.get_record() + elif conference.date_begin - timedelta(hours=1) < now < conference.date_begin: + # conference can be joined + webclass_status = "almost" + else: + webclass_status = "ingoing" + context['webclass_status'] = webclass_status else: content_type = ContentType.objects.get(app_label="teleforma", model="conference") context['livestreams'] = conference.livestream.all() context['host'] = get_host(self.request) + all_courses = get_courses(self.request.user) + context['all_courses'] = all_courses access = get_course_access(conference, all_courses) if not access: context['access_error'] = access_error context['message'] = contact_message - all_courses = get_courses(self.request.user) - context['all_courses'] = all_courses + context['notes'] = conference.notes.all().filter(author=self.request.user) context['type'] = conference.course_type context['course'] = conference.course @@ -499,6 +516,29 @@ class ConferenceView(DetailView): return super(ConferenceView, self).dispatch(*args, **kwargs) +def join_webclass(request, pk): + conference = Conference.objects.get(pk=int(pk)) + user = request.user + authorized = False + + # staff or professor ? + is_professor = len(user.professor.all()) >= 1 + is_staff = user.is_staff or user.is_superuser + if is_professor or is_staff: + authorized = True + + # student registered ? + if not authorized: + auditor = user.auditor.get() + if auditor in conference.auditor.all(): + authorized = True + + if authorized: + return redirect(conference.get_join_webclass_url(user)) + else: + return HttpResponse('Unauthorized', status=401) + + class ConferenceRecordView(FormView): "Conference record form : telecaster module required" diff --git a/teleforma/views/pro.py b/teleforma/views/pro.py index d521c73e..367eed3b 100644 --- a/teleforma/views/pro.py +++ b/teleforma/views/pro.py @@ -58,6 +58,8 @@ from cStringIO import StringIO from xhtml2pdf import pisa +import time +import json import csv from forms_builder.forms.forms import FormForForm @@ -65,6 +67,7 @@ from forms_builder.forms.models import Form from forms_builder.forms.signals import form_invalid, form_valid from pbcart.models import Cart from quiz.views import QuizTake +from django.views.decorators.csrf import csrf_exempt REVISION_DATE_FILTER = datetime.datetime(2015,2,2) @@ -146,10 +149,12 @@ class SeminarAccessMixin(object): return super(SeminarAccessMixin, self).render_to_response(context) + + class SeminarRevisionMixin(object): - @jsonrpc_method('teleforma.seminar_load') - def seminar_load(request, id, username): + @staticmethod + def seminar_do_load(request, id, username): seminar = Seminar.objects.get(id=id) user = User.objects.get(username=username) all_revisions = SeminarRevision.objects.filter(user=user, date__gte=REVISION_DATE_FILTER, date_modified=None) @@ -166,9 +171,12 @@ class SeminarRevisionMixin(object): r = SeminarRevision(seminar=seminar, user=user) r.save() - @jsonrpc_method('teleforma.seminar_unload') - def seminar_unload(request, id, username): - import pdb;pdb.set_trace() + @jsonrpc_method('teleforma.seminar_load') + def seminar_load(request, id, username): + return SeminarRevisionMixin.seminar_do_load(request, id, username) + + @staticmethod + def seminar_do_unload(request, id, username): seminar = Seminar.objects.get(id=id) user = User.objects.get(username=username) all_revisions = SeminarRevision.objects.filter(user=user, date__gte=REVISION_DATE_FILTER, date_modified=None) @@ -191,6 +199,9 @@ class SeminarRevisionMixin(object): revision.date_modified = now revision.save() + @jsonrpc_method('teleforma.seminar_unload') + def seminar_unload(request, id, username): + return SeminarRevisionMixin.seminar_do_unload(request, id, username) class SeminarView(SeminarAccessMixin, DetailView): @@ -931,3 +942,48 @@ class QuizQuestionView(SeminarAccessMixin, SeminarRevisionMixin, QuizTake): doc.save() return render(self.request, 'quiz/result.html', results) + +def process_webclass_bbb_webhook(request, event): + meeting = event["attributes"]["meeting"]["external-meeting-id"] + user = event["attributes"]["user"]["external-user-id"] + conf = Conference.objects.get(webclass_id = meeting) + user = User.objects.get(username = user) + seminar = Seminar.objects.get(conference = conf) + + mixin = SeminarRevisionMixin() + + if event["id"] == "user-joined": + print("JOIN", seminar, conf, user) + mixin.seminar_do_load(request, seminar.pk, user.username) + print("JOIN DONE", seminar, conf, user) + else: + print("LEAVE", seminar, conf, user) + mixin.seminar_do_unload(request, seminar.pk, user.username) + print("LEAVE DONE", seminar, conf, user) + +@csrf_exempt +def webclass_bbb_webhook(request): + """ + Hook for the Big Blue Button webhooks + """ + if not settings.BBB_USE_WEBHOOKS: + return HttpResponse("Web hooks disabled") + + if request.method != "POST": + raise PermissionDenied + + event = json.loads(request.POST['event']) + event = event[0]["data"] + if event["type"] != "event": + raise PermissionDenied + print(event["id"]) + if event["id"] not in ('user-joined', 'user-left'): + return HttpResponse("ok") + + try: + process_webclass_bbb_webhook(request, event) + except Exception as e: + logger.exception("Error processing event %s from BBB for %s on %s" % (event["id"], user.username, seminar.pk)) + + return HttpResponse("ok") + -- 2.39.5