Commit 17865dd4 by Nate Hardison Committed by Joe Blaylock

Move Candy.js widget to its own iframe

Moving the Jabber widget to its own iframe allows us to pop it out
and in of the courseware. Refactoring more of the code (static
assets, templates, etc.) keeps the Jabber app more self-contained
so that it doesn't clutter up the main repo code.

This code needs some Jasmine tests and view tests. It also needs
some extra configuration in the settings and URLs files, namely:

* Must add jabber/templates to MAKO_TEMPLATES
* Must add jabber/static to STATICFILES_DIRS
* Must add route for ?P<course_id>/chat to lms/urls.py

Move Candy.js static assets to Jabber app

Refactor the Candy.js static assets to the Jabber app to keep them
out of the main codebase. This will help modularize the chat widget
so that it's easier to just not include it by default, if desired.

Move registered_for_course out of courseware.views

The Jabber app needs to use this function, and it seems more fitting
to import the function from a non-view module, like the courses
module. So move that function there, and update imports and tests
appropriately.
parent b61869f7
......@@ -24,6 +24,7 @@ from static_replace import replace_static_urls
from courseware.access import has_access
import branding
from xmodule.modulestore.exceptions import ItemNotFoundError
from student.models import CourseEnrollment
log = logging.getLogger(__name__)
......@@ -298,3 +299,15 @@ def sort_by_announcement(courses):
courses = sorted(courses, key=key)
return courses
def registered_for_course(course, user):
"""
Return CourseEnrollment if user is registered for course, else False
"""
if user is None:
return False
if user.is_authenticated():
return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
else:
return False
from mock import MagicMock
import datetime
from django.test import TestCase
from django.conf import settings
from django.contrib.auth.models import User
from django.test.client import RequestFactory
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
import courseware.views as views
from pytz import UTC
class Stub():
pass
# This part is required for modulestore() to work properly
def xml_store_config(data_dir):
return {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
class CoursesTestCase(TestCase):
def setUp(self):
self.user = User.objects.create(username='dummy', password='123456',
email='test@mit.edu')
self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC)
self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id,
created=self.date)[0]
self.location = ['tag', 'org', 'course', 'category', 'name']
self._MODULESTORES = {}
# This is a CourseDescriptor object
self.toy_course = modulestore().get_course('edX/toy/2012_Fall')
self.request_factory = RequestFactory()
chapter = 'Overview'
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_id, chapter)
def test_registered_for_course(self):
self.assertFalse(views.registered_for_course('Basketweaving', None))
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertFalse(views.registered_for_course('dummy', mock_user))
mock_course = MagicMock()
mock_course.id = self.course_id
self.assertTrue(views.registered_for_course(mock_course, self.user))
......@@ -101,15 +101,6 @@ class ViewsTestCase(TestCase):
self.assertRaises(Http404, views.redirect_to_course_position,
mock_module)
def test_registered_for_course(self):
self.assertFalse(views.registered_for_course('Basketweaving', None))
mock_user = MagicMock()
mock_user.is_authenticated.return_value = False
self.assertFalse(views.registered_for_course('dummy', mock_user))
mock_course = MagicMock()
mock_course.id = self.course_id
self.assertTrue(views.registered_for_course(mock_course, self.user))
def test_jump_to_invalid(self):
request = self.request_factory.get(self.chapter_url)
self.assertRaisesRegexp(Http404, 'Invalid location', views.jump_to,
......
......@@ -18,7 +18,9 @@ from django.views.decorators.cache import cache_control
from courseware import grades
from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement)
get_courses_by_university,
registered_for_course,
sort_by_announcement)
import courseware.tabs as tabs
from courseware.masquerade import setup_masquerade
from courseware.model_data import ModelDataCache
......@@ -35,7 +37,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
from xmodule.modulestore.search import path_to_location
import comment_client
import jabber.utils
log = logging.getLogger("mitx.courseware")
......@@ -298,16 +299,6 @@ def index(request, course_id, chapter=None, section=None,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
# Only show the chat if it's enabled both in the settings and
# by the course.
context['show_chat'] = course.show_chat and settings.MITX_FEATURES.get('ENABLE_CHAT')
if context['show_chat']:
context['chat'] = {
'bosh_url': jabber.utils.get_bosh_url(),
'course_room': jabber.utils.get_room_name_for_course(course.id),
'username': "%s@%s" % (user.username, settings.JABBER.get('HOST')),
'password': jabber.utils.get_or_create_password_for_user(user.username)
}
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None:
......@@ -511,18 +502,6 @@ def syllabus(request, course_id):
'staff_access': staff_access, })
def registered_for_course(course, user):
"""
Return CourseEnrollment if user is registered for course, else False
"""
if user is None:
return False
if user.is_authenticated():
return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
else:
return False
@ensure_csrf_cookie
@cache_if_anonymous
def course_about(request, course_id):
......
/**
* candy_shop.js
* -------------
* This is where we can hook into the Candy.js chat widget to
* provide our own, custom UI functionality. See
* http://candy-chat.github.io/candy/#customization for more info.
*/
var CandyShop = (function(self) { return self; }(CandyShop || {}));
CandyShop.EdX = (function(self, Candy, $) {
self.init = function() {
// When a new chat room is added, update the corresponding
// tab's title text and CSS class
Candy.View.Event.Room.onAdd = function(roomPane) {
var roomJid = roomPane['roomJid'];
var roomType = roomPane['type'];
var roomTabClass;
if (roomType === 'groupchat') {
roomTabClass = 'icon-group';
roomTabCloseClass = 'hidden';
} else {
roomTabClass = 'icon-user';
roomTabCloseClass = '';
}
var roomTab = $('#chat-tabs li[data-roomjid="' + roomJid + '"]').find('.label');
roomTab.attr('title', roomTab.text()).html('<em class="' + roomTabClass + '"></em>');
}
// When a user joins or leaves the chat, update the roster
// in the sidebar accordingly
Candy.View.Event.Roster.onUpdate = function(vars) {
var roomJid = vars['roomJid'];
var userNick = vars['user']['data']['nick'];
var userJid = vars['user']['data']['jid'];
var userObject = $('#chat-rooms .room-pane[data-roomjid="' + roomJid + '"] .roster-pane .user[data-jid="' + userJid + '"]');
if ($(userObject).hasClass('me')) {
$(userObject).find('.label').html('<em class="icon-flag"></em> ' + userNick);
$('#chat-rooms .room-pane[data-roomjid="' + roomJid + '"] .roster-pane').prepend($(userObject));
}
}
};
return self;
}(CandyShop.EdX || {}, Candy, jQuery));
<%namespace name='static' file='/static_content.html'/>
<html>
<head>
<link rel="stylesheet" href="${static.url('css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css')}" />
## It'd be better to have this in a place like lms/css/vendor/candy,
## but the candy_res/ folder contains images and other junk, and it
## all needs to stay together for the Candy.js plugin to work.
<link rel="stylesheet" href="${static.url('candy_res/candy_full.css')}" />
## Load in jQuery libs from standard edX locations.
<script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-ui.min.js')}"></script>
## Include the Candy.js libraries. Wooooo.
<script type="text/javascript" src="${static.url('js/vendor/candy_libs/libs.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/candy.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/candy_ui.js')}"></script>
## Include edX-specific Candy plugins
<script type="text/javascript" src="${static.url('js/candy_shop.js')}"></script>
</head>
<body>
## Candy.js renders itself in an element with #candy
<div id="candy"></div>
## Initialize the Candy.js plugin
<script type="text/javascript">
$(document).ready(function() {
Candy.init("${bosh_url}", {
//core: { debug: true, autojoin: ["${course_room}"] },
core: { debug: true, autojoin: ["dev@conference.jabber.class.stanford.edu"] },
view: { resources: "${static.url('candy_res/')}"}
});
## Initialize edX-specific Candy plugins
//CandyShop.EdX.init();
Candy.Core.connect("${username}", "${password}");
});
</script>
</body>
</html>
import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from courseware.courses import get_course_with_access, \
registered_for_course
from jabber.utils import get_bosh_url, get_room_name_for_course,\
get_or_create_password_for_user
from mitxmako.shortcuts import render_to_response
# TODO: should this be standardized somewhere?
log = logging.getLogger("mitx.courseware")
@login_required
@ensure_csrf_cookie
def chat(request, course_id):
"""
Displays a Jabber chat widget.
Arguments:
- request : HTTP request
- course_id : course id (str: ORG/course/URL_NAME)
"""
user = request.user
course = get_course_with_access(user, course_id, 'load', depth=2)
# This route should not exist if chat is disabled by the settings
if not settings.MITX_FEATURES.get('ENABLE_CHAT'):
log.debug("""
User %s tried to enter course %s chat, but chat is not
enabled in the settings
""", user, course.location.url())
return redirect(reverse('about_course', args=[course.id]))
# Don't show chat if course doesn't have it enabled
if not course.show_chat:
log.debug("""
User %s tried to enter course %s chat, but chat is not
enabled for that course
""", user, course.location.url())
return redirect(reverse('about_course', args=[course.id]))
# Ensure that the user is registered before showing chat
registered = registered_for_course(course, user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug("""
User %s tried to enter course %s chat but is not enrolled
""", user, course.location.url())
return redirect(reverse('about_course', args=[course.id]))
# Set up all of the chat context necessary to render
context = {
'bosh_url': get_bosh_url(),
'course_room': get_room_name_for_course(course.id),
'username': "%s@%s" % (user.username, settings.JABBER.get('HOST')),
'password': get_or_create_password_for_user(user.username)
}
return render_to_response("chat.html", context)
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">courseware ${course.css_class}</%block>
<%block name="title"><title>${course.number} Courseware</title></%block>
<%block name="headextra">
<%static:css group='course'/>
<%include file="../discussion/_js_head_dependencies.html" />
% if show_chat:
<link rel="stylesheet" href="${static.url('css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css')}" />
## It'd be better to have this in a place like lms/css/vendor/candy,
## but the candy_res/ folder contains images and other junk, and it
## all needs to stay together for the Candy.js plugin to work.
<link rel="stylesheet" href="${static.url('candy_res/candy_full.css')}" />
% endif
</%block>
<%block name="js_extra">
......@@ -116,19 +110,9 @@
</script>
% endif
% if show_chat:
<script type="text/javascript" src="${static.url('js/vendor/candy_libs/libs.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/candy.min.js')}"></script>
% if settings.MITX_FEATURES.get("ENABLE_CHAT") and course.show_chat:
<script type="text/javascript">
// initialize the Candy.js plugin
$(document).ready(function() {
Candy.init("${chat['bosh_url']}", {
core: { debug: true, autojoin: ["${chat['course_room']}"] },
view: { resources: "${static.url('candy_res/')}"}
});
Candy.Core.connect("${chat['username']}", "${chat['password']}");
$(function() {
// show/hide the chat widget
$('#chat-toggle').click(function() {
var toggle = $(this);
......@@ -146,6 +130,8 @@
}
toggle.toggleClass('closed');
});
// TODO: add pop-in/pop-out functionality here
});
</script>
% endif
......@@ -190,17 +176,19 @@
</div>
</section>
% if show_chat:
% if settings.MITX_FEATURES.get("ENABLE_CHAT") and course.show_chat:
<div id="chat-wrapper">
<div id="chat-toggle" class="closed">
<span id="chat-open">${_("Open Chat")} <em class="icon-chevron-up"></em></span>
<span id="chat-close">${_("Close Chat")} <em class="icon-chevron-down"></em></span>
</div>
<div id="chat-block">
## The Candy.js plugin wants to render in an element with #candy
<div id="candy"></div>
<div id="chat-frame-wrapper">
## TODO: this link probably needs course ID as a parameter...
<iframe id="chat-block" src="${reverse('chat', kwargs={'course_id': course.id})}"></iframe>
</div>
<a id="chat-popout" href="#" title="${_('Pop-Out Chat Window')}" class="icon-signout"></a>
</div>
<a id="chat-popin" href="#" title="${_('Pop-In Chat Window')}" class="icon-signin"></a>
% endif
% if course.show_calculator:
......
......@@ -325,6 +325,15 @@ if settings.COURSEWARE_ENABLED:
url(r'^masquerade/(?P<marg>.*)$', 'courseware.masquerade.handle_ajax', name="masquerade-switch"),
)
# chat renders inside an iframe so that we can better track
# an active chat user between page requests and pop out the chat
# widget window
if settings.MITX_FEATURES.get('ENABLE_CHAT'):
urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/chat/?$',
'jabber.views.chat', name="chat"),
)
# discussion forums live within courseware, so courseware must be enabled first
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment