]> git.parisson.com Git - teleforma.git/commitdiff
WIP
authorYoan Le Clanche <yoanl@pilotsystems.net>
Thu, 7 May 2020 12:10:00 +0000 (14:10 +0200)
committerYoan Le Clanche <yoanl@pilotsystems.net>
Thu, 7 May 2020 12:10:00 +0000 (14:10 +0200)
teleforma/bbb_utils.py [new file with mode: 0644]
teleforma/management/commands/teleforma-revisions-from-bbb.py [new file with mode: 0644]
teleforma/models/core.py [changed mode: 0644->0755]
teleforma/templates/teleforma/course_webclass.html
teleforma/templates/teleforma/seminar_detail.html
teleforma/urls.py
teleforma/views/core.py
teleforma/views/pro.py

diff --git a/teleforma/bbb_utils.py b/teleforma/bbb_utils.py
new file mode 100644 (file)
index 0000000..68d4e9f
--- /dev/null
@@ -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 (file)
index 0000000..994cc47
--- /dev/null
@@ -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 <duration in seconds>" % (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()
old mode 100644 (file)
new mode 100755 (executable)
index 784b9f0..4c53174
@@ -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"""
index a154f53774c9a77921f1c99c1674891eb47d3d51..8107b636427f7da18e4f66fd99bde9da82f14935 100644 (file)
@@ -9,13 +9,13 @@
 <script type="text/javascript">
 {% if show_record %}
 $(document).ready(function(){
-    json(['{{seminar.id}}','{{user.username}}'],'teleforma.seminar_load', function(){return null;});
+    onLoadSeminar('{{seminar.id}}','{{user.username}}')
 });
 
 $(window).ready(function(){
     $(window).bind('beforeunload', function(){
         console.log('unload')
-        json_sync(['{{seminar.id}}','{{user.username}}'],'teleforma.seminar_unload',function(){return null;});
+        onUnloadSeminar('{{seminar.id}}','{{user.username}}')        
     });
 });
 {% endif %}
@@ -60,11 +60,31 @@ $(window).ready(function(){
 </div>
 
 
-Rejoindre la webclasse : <a href="{{ webclass_url }}">webclass_url</a>
 
-<div class="media">
-<!-- include iframe -->
-</div>
+
+
+{% if webclass_status == 'past' %}
+    {% if record %}
+        <p>La conférence est terminée, vous pouvez la revoir ci-dessous.
+        <div class="media">
+        <iframe src="{{record.url}}" width="100%" height="500"></iframe>
+        </div>
+    {% else %}
+        <p>La conférence est terminée. L'enregistrement n'est pas encore ligne.
+    {% endif %}
+{% elif webclass_status == 'ingoing' %}
+    <p>La conférence est en cours.</p>
+    <a href="{% url teleforma-conference-join conference.id %}">Cliquez ici pour rejoindre la conférence</a>
+{% elif webclass_status == 'almost' %}
+    <p>La conférence va bientôt démarrer. </p>
+    <a href="{% url teleforma-conference-join conference.id %}">Cliquez ici pour rejoindre la conférence</a>
+{% elif webclass_status == 'future' %}
+    <p>La conférence est prévue pour le {{ conference.date_begin }}. Revenez ici à ce moment là pour pouvoir la rejoindre.</p>
+{% endif %}
+
+
+<p>DEBUG : <a href="{% url teleforma-conference-join conference.id %}">Cliquez ici pour rejoindre la conférence</a></p>
+
 
 {% block general_info %}
 <div class="course_content" id="media_infos">
index b35cb683af144648f8851bd9461eba7b5ef8ddb9..f86b0abee4bfed9d36edbcefb6910421fd2829d1 100644 (file)
@@ -55,21 +55,12 @@ var seminarUtils = {
                 });
          },
         {% endif %}
-
-        load : function(id, username){
-            json([id, username],'teleforma.seminar_load', function(){return null;});
-         },
-
-        unload : function(id, username){
-            json_sync([id, username],'teleforma.seminar_unload',function(){return null;});
-         },
         }
 
 var f = seminarUtils;
-
-$(document).ready(function( ){
-      f.load('{{seminar.id}}','{{user.username}}');
-    });
+$(document).ready(function(){
+    onLoadSeminar('{{seminar.id}}','{{user.username}}')
+});
 
 $(window).ready(function( ){
   {% if user.is_staff %}
@@ -85,10 +76,11 @@ $(window).ready(function( ){
             }
         });
   {% endif %}
-      $(window).bind('beforeunload', function(){
-            f.unload('{{seminar.id}}','{{user.username}}');
-          });
+    $(window).bind('beforeunload', function(){
+        console.log('unload')
+        onUnloadSeminar('{{seminar.id}}','{{user.username}}')        
     });
+});
 
 </script>
 
index cb065723d737c284620ce0c04b019b99737cd4aa..605bb7889e1550d3893fc0d47a44dd942e43490a 100644 (file)
@@ -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<pk>.*)/join/$',
+        join_webclass,
+        name="teleforma-conference-join"),
+    
 
     # Questions
     url(r'^desk/seminars/(?P<id>.*)/questions/(?P<pk>.*)/$', AnswerView.as_view(),
@@ -166,4 +170,8 @@ urlpatterns = patterns('',
 
     url(r'^desk/test/(?P<pk>.*)/$', AnswerDetailViewTest.as_view(), name="test"),
 
+    # Webclass hook
+    url(r'^webclass_bbb_webhook/', webclass_bbb_webhook,
+        name="webclass_bbb_webhook"),
+                       
 )
index 0f4ec004418feda5080a02b5c142d4a9405acd99..b5d2d571b2d8929eb58da5f250a3e2ab33b405a3 100644 (file)
@@ -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"
 
index d521c73e1142b84728e45ba6c82ee6bb6615cade..367eed3b6e9179c92d11d4e0e08426a78c20edfd 100644 (file)
@@ -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")
+