--- /dev/null
+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)
+
--- /dev/null
+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()
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]
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
+
@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"""
<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 %}
</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">
});
},
{% 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 %}
}
});
{% endif %}
- $(window).bind('beforeunload', function(){
- f.unload('{{seminar.id}}','{{user.username}}');
- });
+ $(window).bind('beforeunload', function(){
+ console.log('unload')
+ onUnloadSeminar('{{seminar.id}}','{{user.username}}')
});
+});
</script>
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(),
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"),
+
)
import mimetypes
import datetime
+from datetime import timedelta
import random
import urllib
import urllib2
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
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"
from xhtml2pdf import pisa
+import time
+import json
import csv
from forms_builder.forms.forms import FormForForm
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)
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)
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)
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):
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")
+