Commit 3a2c0e37 by stv

Remove chat feature

This was originally contributed upstream by Stanford, circa 2013.

We neither use nor support this feature in its current implementation,
and in fact, we may never have used this production. Until recently, we
had additional chat/Jabber code [1] (in the form of a Jabber djangoapp in
LMS); context there suggests this feature may have never been more than
a prototype. The original author is no longer on the team, so I can't directly
confirm this on our end.

Do you use this feature?

Stanford had already abandoned this Jabber-backed chat implementation,
in favor of an IRC backend, by the time I joined the team in early 2014.

[1] https://github.com/Stanford-Online/edx-platform/commit/dbe52a6b133ab6ff637dea5324f7322da2909070
parent dd5a6acf
...@@ -270,12 +270,6 @@ class CourseFields(object): ...@@ -270,12 +270,6 @@ class CourseFields(object):
scope=Scope.settings, scope=Scope.settings,
deprecated=True # Deprecated because someone would not edit this value within Studio. deprecated=True # Deprecated because someone would not edit this value within Studio.
) )
show_chat = Boolean(
display_name=_("Show Chat Widget"),
help=_("Enter true or false. When true, students can see the chat widget in the course."),
default=False,
scope=Scope.settings
)
tabs = CourseTabList(help="List of tabs to enable in this course", scope=Scope.settings, default=[]) tabs = CourseTabList(help="List of tabs to enable in this course", scope=Scope.settings, default=[])
end_of_course_survey_url = String( end_of_course_survey_url = String(
display_name=_("Course Survey URL"), display_name=_("Course Survey URL"),
......
...@@ -498,10 +498,10 @@ class TestXmlAttributes(XModuleXmlImportTest): ...@@ -498,10 +498,10 @@ class TestXmlAttributes(XModuleXmlImportTest):
assert_equals('value', course.xml_attributes['unknown_attr']) assert_equals('value', course.xml_attributes['unknown_attr'])
def test_known_attribute(self): def test_known_attribute(self):
assert_true(hasattr(CourseDescriptor, 'show_chat')) assert_true(hasattr(CourseDescriptor, 'show_calculator'))
course = self.process_xml(CourseFactory.build(show_chat='true')) course = self.process_xml(CourseFactory.build(show_calculator='true'))
assert_true(course.show_chat) assert_true(course.show_calculator)
assert_not_in('show_chat', course.xml_attributes) assert_not_in('show_calculator', course.xml_attributes)
def test_rerandomize_in_policy(self): def test_rerandomize_in_policy(self):
# Rerandomize isn't a basic attribute of Sequence # Rerandomize isn't a basic attribute of Sequence
......
...@@ -209,7 +209,6 @@ class AdvancedSettingsPage(CoursePage): ...@@ -209,7 +209,6 @@ class AdvancedSettingsPage(CoursePage):
'annotation_token_secret', 'annotation_token_secret',
'showanswer', 'showanswer',
'show_calculator', 'show_calculator',
'show_chat',
'show_reset_button', 'show_reset_button',
'static_asset_path', 'static_asset_path',
'text_customization', 'text_customization',
......
{"course/2014": {"advanced_modules": ["annotatable", "combinedopenended", "peergrading", "lti", "word_cloud"], "show_calculator": true, "display_name": "Manual Smoke Test Course 1", "tabs": [{"type": "courseware", "name": "Courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "textbooks", "name": "Textbooks"}, {"type": "discussion", "name": "Discussion"}, {"type": "wiki", "name": "Wiki"}, {"type": "progress", "name": "Progress"}, {"type": "pdf_textbooks", "name": "Textbooks"}, {"type": "open_ended", "name": "Open Ended Panel"}], "discussion_topics": {"General": {"id": "i4x-ManTestX-ManTest1-course-2014"}}, "start": "2014-06-26T00:00:00Z", "pdf_textbooks": [{"tab_title": "An Example Paper", "id": "0An_Example_Paper", "chapters": [{"url": "/static/1.pdf", "title": "Introduction "}]}], "lti_passports": ["ims:12345:secret"], "show_chat": true}} {"course/2014": {"advanced_modules": ["annotatable", "combinedopenended", "peergrading", "lti", "word_cloud"], "show_calculator": true, "display_name": "Manual Smoke Test Course 1", "tabs": [{"type": "courseware", "name": "Courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "textbooks", "name": "Textbooks"}, {"type": "discussion", "name": "Discussion"}, {"type": "wiki", "name": "Wiki"}, {"type": "progress", "name": "Progress"}, {"type": "pdf_textbooks", "name": "Textbooks"}, {"type": "open_ended", "name": "Open Ended Panel"}], "discussion_topics": {"General": {"id": "i4x-ManTestX-ManTest1-course-2014"}}, "start": "2014-06-26T00:00:00Z", "pdf_textbooks": [{"tab_title": "An Example Paper", "id": "0An_Example_Paper", "chapters": [{"url": "/static/1.pdf", "title": "Introduction "}]}], "lti_passports": ["ims:12345:secret"]}}
\ No newline at end of file
...@@ -362,28 +362,6 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -362,28 +362,6 @@ class ViewsTestCase(ModuleStoreTestCase):
else: else:
self.assertNotContains(result, "Classes End") self.assertNotContains(result, "Classes End")
def test_chat_settings(self):
mock_user = MagicMock()
mock_user.username = "johndoe"
mock_course = MagicMock()
mock_course.id = "a/b/c"
# Stub this out in the case that it's not in the settings
domain = "jabber.edx.org"
settings.JABBER_DOMAIN = domain
chat_settings = views.chat_settings(mock_course, mock_user)
# Test the proper format of all chat settings
self.assertEqual(chat_settings['domain'], domain)
self.assertEqual(chat_settings['room'], "a-b-c_class")
self.assertEqual(chat_settings['username'], "johndoe@%s" % domain)
# TODO: this needs to be changed once we figure out how to
# generate/store a real password.
self.assertEqual(chat_settings['password'], "johndoe@%s" % domain)
def test_submission_history_accepts_valid_ids(self): def test_submission_history_accepts_valid_ids(self):
# log into a staff account # log into a staff account
admin = AdminFactory() admin = AdminFactory()
......
...@@ -281,36 +281,6 @@ def save_positions_recursively_up(user, request, field_data_cache, xmodule, cour ...@@ -281,36 +281,6 @@ def save_positions_recursively_up(user, request, field_data_cache, xmodule, cour
current_module = parent current_module = parent
def chat_settings(course, user):
"""
Returns a dict containing the settings required to connect to a
Jabber chat server and room.
"""
domain = getattr(settings, "JABBER_DOMAIN", None)
if domain is None:
log.warning('You must set JABBER_DOMAIN in the settings to '
'enable the chat widget')
return None
return {
'domain': domain,
# Jabber doesn't like slashes, so replace with dashes
'room': "{ID}_class".format(ID=course.id.replace('/', '-')),
'username': "{USER}@{DOMAIN}".format(
USER=user.username, DOMAIN=domain
),
# TODO: clearly this needs to be something other than the username
# should also be something that's not necessarily tied to a
# particular course
'password': "{USER}@{DOMAIN}".format(
USER=user.username, DOMAIN=domain
),
}
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -470,18 +440,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -470,18 +440,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# empty first section and a second section with content # empty first section and a second section with content
return redirect_to_course_position(course_module, CONTENT_DEPTH) return redirect_to_course_position(course_module, CONTENT_DEPTH)
# Only show the chat if it's enabled by the course and in the
# settings.
show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
if show_chat:
context['chat'] = chat_settings(course, request.user)
# If we couldn't load the chat settings, then don't show
# the widget in the courseware.
if context['chat'] is None:
show_chat = False
context['show_chat'] = show_chat
chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter) chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
if chapter_descriptor is not None: if chapter_descriptor is not None:
save_child_position(course_module, chapter) save_child_position(course_module, chapter)
......
...@@ -224,10 +224,6 @@ FEATURES = { ...@@ -224,10 +224,6 @@ FEATURES = {
# for load testing # for load testing
'AUTOMATIC_AUTH_FOR_TESTING': False, 'AUTOMATIC_AUTH_FOR_TESTING': False,
# Toggle to enable chat availability (configured on a per-course
# basis in Studio)
'ENABLE_CHAT': False,
# Allow users to enroll with methods other than just honor code certificates # Allow users to enroll with methods other than just honor code certificates
'MULTIPLE_ENROLLMENT_ROLES': False, 'MULTIPLE_ENROLLMENT_ROLES': False,
......
This folder referenced in: lms/templates/courseware/courseware.html
Notes from that file:
## 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.
Simple Smileys is a set of 49 clean, free as in freedom, Public Domain smileys.
For more packages or older versions, visit http://simplesmileys.org
/*
* Date Format 1.2.3
* (c) 2007-2009 Steven Levithan <stevenlevithan.com>
* MIT license
*
* Includes enhancements by Scott Trenda <scott.trenda.net>
* and Kris Kowal <cixar.com/~kris.kowal/>
*
* Accepts a date, a mask, or a date and a mask.
* Returns a formatted version of the given date.
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*
* @link http://blog.stevenlevithan.com/archives/date-time-format
*/
var dateFormat = function () {
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
timezoneClip = /[^-+\dA-Z]/g,
pad = function (val, len) {
val = String(val);
len = len || 2;
while (val.length < len) val = "0" + val;
return val;
};
// Regexes and supporting functions are cached through closure
return function (date, mask, utc) {
var dF = dateFormat;
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
mask = date;
date = undefined;
}
// Passing date through Date applies Date.parse, if necessary
date = date ? new Date(date) : new Date;
if (isNaN(date)) throw SyntaxError("invalid date");
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
// Allow setting the utc argument via the mask
if (mask.slice(0, 4) == "UTC:") {
mask = mask.slice(4);
utc = true;
}
var _ = utc ? "getUTC" : "get",
d = date[_ + "Date"](),
D = date[_ + "Day"](),
m = date[_ + "Month"](),
y = date[_ + "FullYear"](),
H = date[_ + "Hours"](),
M = date[_ + "Minutes"](),
s = date[_ + "Seconds"](),
L = date[_ + "Milliseconds"](),
o = utc ? 0 : date.getTimezoneOffset(),
flags = {
d: d,
dd: pad(d),
ddd: dF.i18n.dayNames[D],
dddd: dF.i18n.dayNames[D + 7],
m: m + 1,
mm: pad(m + 1),
mmm: dF.i18n.monthNames[m],
mmmm: dF.i18n.monthNames[m + 12],
yy: String(y).slice(2),
yyyy: y,
h: H % 12 || 12,
hh: pad(H % 12 || 12),
H: H,
HH: pad(H),
M: M,
MM: pad(M),
s: s,
ss: pad(s),
l: pad(L, 3),
L: pad(L > 99 ? Math.round(L / 10) : L),
t: H < 12 ? "a" : "p",
tt: H < 12 ? "am" : "pm",
T: H < 12 ? "A" : "P",
TT: H < 12 ? "AM" : "PM",
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
};
return mask.replace(token, function ($0) {
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
});
};
}();
// Some common format strings
dateFormat.masks = {
"default": "ddd mmm dd yyyy HH:MM:ss",
shortDate: "m/d/yy",
mediumDate: "mmm d, yyyy",
longDate: "mmmm d, yyyy",
fullDate: "dddd, mmmm d, yyyy",
shortTime: "h:MM TT",
mediumTime: "h:MM:ss TT",
longTime: "h:MM:ss TT Z",
isoDate: "yyyy-mm-dd",
isoTime: "HH:MM:ss",
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
// Internationalization strings
dateFormat.i18n = {
dayNames: [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
],
monthNames: [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
]
};
// For convenience...
Date.prototype.format = function (mask, utc) {
return dateFormat(this, mask, utc);
};
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
$(window).load(function() {
var isInIframe = (window.location != window.parent.location) ? true : false;
if (isInIframe) {
$('#chat-pane #chat-tabs').prepend('<div id="chat-expand-arrow"><em class="icon-chevron-right"></em></div>');
} else {
$('#candy').addClass('poppedOut').append('<a href="#" onclick="event.preventDefault();" title="Pop-In Chat Window" class="icon-signin" id="chatPopin"></a>');
}
var collapseMessageForm = function() {
$('#candy').animate({width: '230px'}, 'slow', function() {
$('#chat-expand-arrow em').toggleClass('icon-chevron-left').toggleClass('icon-chevron-right');
$('#chat-pane').toggleClass('collapsed-message-pane');
});
$('#chat-pane .roster-pane').animate({top: '0px'}, 'slow');
$('#chat-rooms .message-pane-wrapper, #chat-rooms .message-form-wrapper, form.message-form').fadeOut('slow');
}
var expandMessageForm = function() {
$('#chat-pane').toggleClass('collapsed-message-pane');
$('#candy').animate({width: '100%'}, 'slow', function() {
$('#chat-expand-arrow em').toggleClass('icon-chevron-left').toggleClass('icon-chevron-right');
});
$('#chat-pane .roster-pane').animate({top: '30px'}, 'slow');
$('#chat-rooms .message-pane-wrapper, #chat-rooms .message-form-wrapper, form.message-form').fadeIn('slow');
}
var activeTab;
$('#chat-expand-arrow').click(function() {
if ($('#chat-pane').hasClass('collapsed-message-pane')) {
activeTab.addClass('active');
expandMessageForm();
} else {
activeTab = $('#chat-tabs li.active');
$('#chat-tabs li').removeClass('active');
collapseMessageForm();
}
});
$('#chat-tabs').click(function(event) {
if ($(this).has(event.target).length && $('#chat-pane').hasClass('collapsed-message-pane')) {
expandMessageForm();
}
});
$('#chatPopin').click(function() {
window.close();
});
});
...@@ -30,7 +30,6 @@ ...@@ -30,7 +30,6 @@
@import 'course/modules/student-notes'; // student notes @import 'course/modules/student-notes'; // student notes
@import 'course/modules/calculator'; // calculator utility @import 'course/modules/calculator'; // calculator utility
@import 'course/modules/timer'; // timer @import 'course/modules/timer'; // timer
@import 'course/modules/chat'; // chat utility
// course - wiki // course - wiki
@import "course/wiki/basic-html"; @import "course/wiki/basic-html";
......
// LMS -- modules -- chat
// ====================
#chat-wrapper {
position: fixed;
bottom: 0;
left: 0;
z-index: 1000;
}
#chat-toggle {
position: absolute;
left: 0;
top: -45px;
height: 25px;
width: 170px;
padding: 10px 15px;
background: #333;
border-top-right-radius: 10px;
span {
color: #dcdcdc;
font-weight: bold;
font-size: 18px;
}
cursor: pointer;
}
#chat-toggle:hover,
#chat-toggle:focus {
text-decoration: none;
}
#chat-open {
display: inline;
}
#chat-close {
display: none;
}
#chat-toggle em {
position: absolute;
right: 20px;
top: 12px;
font-size: 28px !important;
}
#chat-block {
position: relative;
float: left;
width: 600px;
height: 0px;
bottom: 0;
right: 0;
margin: 0 !important;
border: none !important;
display: none;
}
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<div id="chat-wrapper">
<div id="chat-toggle" class="closed">
<span id="chat-open">${_('Open Chat')} <em class="icon fa fa-chevron-up"></em></span>
<span id="chat-close">${_('Close Chat')} <em class="icon fa fa-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>
</div>
...@@ -41,14 +41,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -41,14 +41,6 @@ ${page_title_breadcrumbs(course_name())}
<%block name="nav_skip">${"#seq_content" if section_title else "#course-content"}</%block> <%block name="nav_skip">${"#seq_content" if section_title else "#course-content"}</%block>
<%include file="../discussion/_js_head_dependencies.html" /> <%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
${fragment.head_html()} ${fragment.head_html()}
</%block> </%block>
...@@ -92,40 +84,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -92,40 +84,6 @@ ${page_title_breadcrumbs(course_name())}
}); });
</script> </script>
% 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>
<script type="text/javascript">
// initialize the Candy.js plugin
$(document).ready(function() {
Candy.init("http://${chat['domain']}:5280/http-bind/", {
core: { debug: true, autojoin: ["${chat['room']}@conference.${chat['domain']}"] },
view: { resources: "${static.url('candy_res/')}"}
});
Candy.Core.connect("${chat['username']}", "${chat['password']}");
// show/hide the chat widget
$('#chat-toggle').click(function() {
var toggle = $(this);
if (toggle.hasClass('closed')) {
$('#chat-block').show().animate({height: '400px'}, 'slow', function() {
$('#chat-open').hide();
$('#chat-close').show();
});
} else {
$('#chat-block').animate({height: '0px'}, 'slow', function() {
$('#chat-open').show();
$('#chat-close').hide();
$(this).hide(); // do this at the very end
});
}
toggle.toggleClass('closed');
});
});
</script>
% endif
${fragment.foot_html()} ${fragment.foot_html()}
</%block> </%block>
...@@ -137,11 +95,6 @@ ${fragment.foot_html()} ...@@ -137,11 +95,6 @@ ${fragment.foot_html()}
</div> </div>
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}"> <nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}">
## Utility: Chat
% if show_chat:
<%include file="/chat/toggle_chat.html" />
% endif
## Utility: Notes ## Utility: Notes
% if is_edxnotes_enabled(course): % if is_edxnotes_enabled(course):
<%include file="/edxnotes/toggle_notes.html" args="course=course"/> <%include file="/edxnotes/toggle_notes.html" args="course=course"/>
......
...@@ -59,14 +59,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -59,14 +59,6 @@ ${page_title_breadcrumbs(course_name())}
<%block name="nav_skip">${"#seq_content" if section_title else "#course-content"}</%block> <%block name="nav_skip">${"#seq_content" if section_title else "#course-content"}</%block>
<%include file="../discussion/_js_head_dependencies.html" /> <%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
${fragment.head_html()} ${fragment.head_html()}
</%block> </%block>
...@@ -116,40 +108,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -116,40 +108,6 @@ ${page_title_breadcrumbs(course_name())}
}); });
</script> </script>
% 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>
<script type="text/javascript">
// initialize the Candy.js plugin
$(document).ready(function() {
Candy.init("http://${chat['domain']}:5280/http-bind/", {
core: { debug: true, autojoin: ["${chat['room']}@conference.${chat['domain']}"] },
view: { resources: "${static.url('candy_res/')}"}
});
Candy.Core.connect("${chat['username']}", "${chat['password']}");
// show/hide the chat widget
$('#chat-toggle').click(function() {
var toggle = $(this);
if (toggle.hasClass('closed')) {
$('#chat-block').show().animate({height: '400px'}, 'slow', function() {
$('#chat-open').hide();
$('#chat-close').show();
});
} else {
$('#chat-block').animate({height: '0px'}, 'slow', function() {
$('#chat-open').show();
$('#chat-close').hide();
$(this).hide(); // do this at the very end
});
}
toggle.toggleClass('closed');
});
});
</script>
% endif
${fragment.foot_html()} ${fragment.foot_html()}
</%block> </%block>
...@@ -247,11 +205,6 @@ ${fragment.foot_html()} ...@@ -247,11 +205,6 @@ ${fragment.foot_html()}
</div> </div>
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}"> <nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}">
## Utility: Chat
% if show_chat:
<%include file="/chat/toggle_chat.html" />
% endif
## Utility: Notes ## Utility: Notes
% if is_edxnotes_enabled(course): % if is_edxnotes_enabled(course):
<%include file="/edxnotes/toggle_notes.html" args="course=course"/> <%include file="/edxnotes/toggle_notes.html" args="course=course"/>
......
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