Commit 248558f1 by Andy Armstrong

Render Discussion tab using web fragments

parent 8fedc08c
...@@ -119,7 +119,7 @@ source, template_path = Loader(engine).load_template_source(path) ...@@ -119,7 +119,7 @@ source, template_path = Loader(engine).load_template_source(path)
}).call(this, require || RequireJS.require); }).call(this, require || RequireJS.require);
% else: % else:
## The "raw" parameter is specified to avoid the URL from being further maninpulated by ## The "raw" parameter is specified to avoid the URL from being further maninpulated by
## static_replace calls (as woudl happen if require_module is used within courseware). ## static_replace calls (as would happen if require_module is used within courseware).
## Without specifying "raw", a call to static_replace would result in the MD5 hash being ## Without specifying "raw", a call to static_replace would result in the MD5 hash being
## being appended more than once, causing the import to fail in production environments. ## being appended more than once, causing the import to fail in production environments.
require(['${staticfiles_storage.url(module_name + ".js") + "?raw" | n, js_escaped_string}'], function () { require(['${staticfiles_storage.url(module_name + ".js") + "?raw" | n, js_escaped_string}'], function () {
......
...@@ -15,6 +15,9 @@ log = logging.getLogger("edx.courseware") ...@@ -15,6 +15,9 @@ log = logging.getLogger("edx.courseware")
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file # `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text _ = lambda text: text
# A list of attributes on course tabs that can not be updated
READ_ONLY_COURSE_TAB_ATTRIBUTES = ['type']
class CourseTab(object): class CourseTab(object):
""" """
...@@ -33,6 +36,12 @@ class CourseTab(object): ...@@ -33,6 +36,12 @@ class CourseTab(object):
# ugettext_noop since the user won't be available in this context. # ugettext_noop since the user won't be available in this context.
title = None title = None
# HTML class to add to the tab page's body, or None if no class it to be added
body_class = None
# Token to identify the online help URL, or None if no help is provided
online_help_token = None
# Class property that specifies whether the tab can be hidden for a particular course # Class property that specifies whether the tab can be hidden for a particular course
is_hideable = False is_hideable = False
...@@ -70,7 +79,7 @@ class CourseTab(object): ...@@ -70,7 +79,7 @@ class CourseTab(object):
Args: Args:
tab_dict (dict) - a dictionary of parameters used to build the tab. tab_dict (dict) - a dictionary of parameters used to build the tab.
""" """
super(CourseTab, self).__init__()
self.name = tab_dict.get('name', self.title) self.name = tab_dict.get('name', self.title)
self.tab_id = tab_dict.get('tab_id', getattr(self, 'tab_id', self.type)) self.tab_id = tab_dict.get('tab_id', getattr(self, 'tab_id', self.type))
self.course_staff_only = tab_dict.get('course_staff_only', False) self.course_staff_only = tab_dict.get('course_staff_only', False)
...@@ -80,6 +89,13 @@ class CourseTab(object): ...@@ -80,6 +89,13 @@ class CourseTab(object):
@property @property
def link_func(self): def link_func(self):
"""
Returns a function that will determine a course URL for this tab.
The returned function takes two arguments:
course (Course) - the course in question.
view_name (str) - the name of the view.
"""
return self.tab_dict.get('link_func', link_reverse_func(self.view_name)) return self.tab_dict.get('link_func', link_reverse_func(self.view_name))
@classmethod @classmethod
...@@ -107,16 +123,8 @@ class CourseTab(object): ...@@ -107,16 +123,8 @@ class CourseTab(object):
This method allows callers to access CourseTab members with the d[key] syntax as is done with This method allows callers to access CourseTab members with the d[key] syntax as is done with
Python dictionary objects. Python dictionary objects.
""" """
if key == 'name': if hasattr(self, key):
return self.name return getattr(self, key, None)
elif key == 'type':
return self.type
elif key == 'tab_id':
return self.tab_id
elif key == 'is_hidden':
return self.is_hidden
elif key == 'course_staff_only':
return self.course_staff_only
else: else:
raise KeyError('Key {0} not present in tab {1}'.format(key, self.to_json())) raise KeyError('Key {0} not present in tab {1}'.format(key, self.to_json()))
...@@ -127,14 +135,8 @@ class CourseTab(object): ...@@ -127,14 +135,8 @@ class CourseTab(object):
Note: the 'type' member can be 'get', but not 'set'. Note: the 'type' member can be 'get', but not 'set'.
""" """
if key == 'name': if hasattr(self, key) and key not in READ_ONLY_COURSE_TAB_ATTRIBUTES:
self.name = value setattr(self, key, value)
elif key == 'tab_id':
self.tab_id = value
elif key == 'is_hidden':
self.is_hidden = value
elif key == 'course_staff_only':
self.course_staff_only = value
else: else:
raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json())) raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json()))
...@@ -236,28 +238,52 @@ class CourseTab(object): ...@@ -236,28 +238,52 @@ class CourseTab(object):
return tab_type(tab_dict=tab_dict) return tab_type(tab_dict=tab_dict)
class ComponentTabMixin(object): class TabFragmentViewMixin(object):
""" """
A mixin for tabs that meet the component API (and can be rendered via Fragments). A mixin for tabs that render themselves as web fragments.
""" """
class_name = None fragment_view_name = None
def __init__(self, tab_dict):
super(TabFragmentViewMixin, self).__init__(tab_dict)
self._fragment_view = None
@property @property
def link_func(self): def link_func(self):
""" Returns a function that returns the course tab's URL. """
# If a view_name is specified, then use the default link function
if self.view_name:
return super(TabFragmentViewMixin, self).link_func
# If not, then use the generic course tab URL
def link_func(course, reverse_func): def link_func(course, reverse_func):
""" Returns a url for a given course and reverse function. """ """ Returns a function that returns the course tab's URL. """
return reverse_func("content_tab", args=[course.id.to_deprecated_string(), self.type]) return reverse_func("course_tab_view", args=[course.id.to_deprecated_string(), self.type])
return link_func return link_func
@property @property
def url_slug(self): def url_slug(self):
return "content_tab/"+self.type """
Returns the slug to be included in this tab's URL.
"""
return "tab/" + self.type
def render_fragment(self, request, course): @property
component = get_storage_class(self.class_name)() def fragment_view(self):
fragment = component.render_component(request, course_id=course.id.to_deprecated_string()) """
return fragment Returns the view that will be used to render the fragment.
"""
if not self._fragment_view:
self._fragment_view = get_storage_class(self.fragment_view_name)()
return self._fragment_view
def render_to_fragment(self, request, course, **kwargs):
"""
Renders this tab to a web fragment.
"""
return self.fragment_view.render_to_fragment(request, course_id=unicode(course.id), **kwargs)
class StaticTab(CourseTab): class StaticTab(CourseTab):
...@@ -270,7 +296,7 @@ class StaticTab(CourseTab): ...@@ -270,7 +296,7 @@ class StaticTab(CourseTab):
def __init__(self, tab_dict=None, name=None, url_slug=None): def __init__(self, tab_dict=None, name=None, url_slug=None):
def link_func(course, reverse_func): def link_func(course, reverse_func):
""" Returns a url for a given course and reverse function. """ """ Returns a function that returns the static tab's URL. """
return reverse_func(self.type, args=[course.id.to_deprecated_string(), self.url_slug]) return reverse_func(self.type, args=[course.id.to_deprecated_string(), self.url_slug])
self.url_slug = tab_dict.get('url_slug') if tab_dict else url_slug self.url_slug = tab_dict.get('url_slug') if tab_dict else url_slug
......
...@@ -9,7 +9,7 @@ from courseware.access import has_access ...@@ -9,7 +9,7 @@ from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam from courseware.entrance_exams import user_must_complete_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.course_tabs import CourseTabPluginManager
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.tabs import ComponentTabMixin, CourseTab, CourseTabList, key_checker from xmodule.tabs import CourseTab, CourseTabList, key_checker
class EnrolledTab(CourseTab): class EnrolledTab(CourseTab):
...@@ -70,14 +70,14 @@ class SyllabusTab(EnrolledTab): ...@@ -70,14 +70,14 @@ class SyllabusTab(EnrolledTab):
return getattr(course, 'syllabus_present', False) return getattr(course, 'syllabus_present', False)
class ProgressTab(ComponentTabMixin, EnrolledTab): class ProgressTab(EnrolledTab):
""" """
The course progress view. The course progress view.
""" """
type = 'progress' type = 'progress'
title = ugettext_noop('Progress') title = ugettext_noop('Progress')
priority = 40 priority = 40
class_name="courseware.views.views.ProgressComponentView" view_name = 'progress'
is_hideable = True is_hideable = True
is_default = False is_default = False
......
...@@ -14,7 +14,7 @@ from courseware.tabs import ( ...@@ -14,7 +14,7 @@ from courseware.tabs import (
) )
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.factories import InstructorFactory, StaffFactory from courseware.tests.factories import InstructorFactory, StaffFactory
from courseware.views.views import get_static_tab_contents, static_tab from courseware.views.views import get_static_tab_fragment, StaticCourseTabView
from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.djangolib.testing.utils import get_mock_request
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -258,16 +258,16 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -258,16 +258,16 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
self.setup_user() self.setup_user()
request = get_mock_request(self.user) request = get_mock_request(self.user)
with self.assertRaises(Http404): with self.assertRaises(Http404):
static_tab(request, course_id='edX/toy', tab_slug='new_tab') StaticCourseTabView().get(request, course_id='edX/toy', tab_slug='new_tab')
def test_get_static_tab_contents(self): def test_get_static_tab_fragment(self):
self.setup_user() self.setup_user()
course = get_course_by_id(self.course.id) course = get_course_by_id(self.course.id)
request = get_mock_request(self.user) request = get_mock_request(self.user)
tab = xmodule_tabs.CourseTabList.get_tab_by_slug(course.tabs, 'new_tab') tab = xmodule_tabs.CourseTabList.get_tab_by_slug(course.tabs, 'new_tab')
# Test render works okay # Test render works okay
tab_content = get_static_tab_contents(request, course, tab) tab_content = get_static_tab_fragment(request, course, tab).content
self.assertIn(self.course.id.to_deprecated_string(), tab_content) self.assertIn(self.course.id.to_deprecated_string(), tab_content)
self.assertIn('static_tab', tab_content) self.assertIn('static_tab', tab_content)
...@@ -276,8 +276,8 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -276,8 +276,8 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
mock_module_render.return_value = MagicMock( mock_module_render.return_value = MagicMock(
render=Mock(side_effect=Exception('Render failed!')) render=Mock(side_effect=Exception('Render failed!'))
) )
static_tab = get_static_tab_contents(request, course, tab) static_tab_content = get_static_tab_fragment(request, course, tab).content
self.assertIn("this module is temporarily unavailable", static_tab) self.assertIn("this module is temporarily unavailable", static_tab_content)
@attr(shard=1) @attr(shard=1)
......
...@@ -7,10 +7,10 @@ from django.utils.translation import ugettext_noop ...@@ -7,10 +7,10 @@ from django.utils.translation import ugettext_noop
from courseware.tabs import EnrolledTab from courseware.tabs import EnrolledTab
import django_comment_client.utils as utils import django_comment_client.utils as utils
from xmodule.tabs import ComponentTabMixin from xmodule.tabs import TabFragmentViewMixin
class DiscussionTab(ComponentTabMixin, EnrolledTab): class DiscussionTab(TabFragmentViewMixin, EnrolledTab):
""" """
A tab for the cs_comments_service forums. A tab for the cs_comments_service forums.
""" """
...@@ -18,9 +18,12 @@ class DiscussionTab(ComponentTabMixin, EnrolledTab): ...@@ -18,9 +18,12 @@ class DiscussionTab(ComponentTabMixin, EnrolledTab):
type = 'discussion' type = 'discussion'
title = ugettext_noop('Discussion') title = ugettext_noop('Discussion')
priority = None priority = None
class_name = 'discussion.views.DiscussionBoardComponentView' view_name = 'discussion.views.forum_form_discussion'
fragment_view_name = 'discussion.views.DiscussionBoardFragmentView'
is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False) is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
is_default = False is_default = False
body_class = 'discussion'
online_help_token = 'discussions'
@classmethod @classmethod
def is_enabled(cls, course, user=None): def is_enabled(cls, course, user=None):
......
...@@ -17,11 +17,11 @@ ...@@ -17,11 +17,11 @@
function($, Backbone, Content, Discussion, DiscussionUtil, DiscussionCourseSettings, DiscussionUser, function($, Backbone, Content, Discussion, DiscussionUtil, DiscussionCourseSettings, DiscussionUser,
NewPostView, DiscussionRouter, DiscussionBoardView) { NewPostView, DiscussionRouter, DiscussionBoardView) {
return function(options) { return function(options) {
var userInfo = options.user_info, var userInfo = options.userInfo,
sortPreference = options.sort_preference, sortPreference = options.sortPreference,
threads = options.threads, threads = options.threads,
threadPages = options.thread_pages, threadPages = options.threadPages,
contentInfo = options.content_info, contentInfo = options.contentInfo,
user = new DiscussionUser(userInfo), user = new DiscussionUser(userInfo),
discussion, discussion,
courseSettings, courseSettings,
...@@ -33,14 +33,14 @@ ...@@ -33,14 +33,14 @@
// TODO: eliminate usage of global variables when possible // TODO: eliminate usage of global variables when possible
DiscussionUtil.loadRoles(options.roles); DiscussionUtil.loadRoles(options.roles);
window.$$course_id = options.courseId; window.$$course_id = options.courseId;
window.courseName = options.course_name; window.courseName = options.courseName;
DiscussionUtil.setUser(user); DiscussionUtil.setUser(user);
window.user = user; window.user = user;
Content.loadContentInfos(contentInfo); Content.loadContentInfos(contentInfo);
// Create a discussion model // Create a discussion model
discussion = new Discussion(threads, {pages: threadPages, sort: sortPreference}); discussion = new Discussion(threads, {pages: threadPages, sort: sortPreference});
courseSettings = new DiscussionCourseSettings(options.course_settings); courseSettings = new DiscussionCourseSettings(options.courseSettings);
// Create the discussion board view // Create the discussion board view
discussionBoardView = new DiscussionBoardView({ discussionBoardView = new DiscussionBoardView({
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
// Set up a router to manage the page's history // Set up a router to manage the page's history
router = new DiscussionRouter({ router = new DiscussionRouter({
courseId: options.courseId, rootUrl: options.rootUrl,
discussion: discussion, discussion: discussion,
courseSettings: courseSettings, courseSettings: courseSettings,
discussionBoardView: discussionBoardView, discussionBoardView: discussionBoardView,
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
DiscussionUtil.loadRoles(options.roles); DiscussionUtil.loadRoles(options.roles);
window.$$course_id = options.courseId; window.$$course_id = options.courseId;
window.courseName = options.course_name; window.courseName = options.courseName;
DiscussionUtil.setUser(user); DiscussionUtil.setUser(user);
window.user = user; window.user = user;
Content.loadContentInfos(contentInfo); Content.loadContentInfos(contentInfo);
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
initialize: function(options) { initialize: function(options) {
Backbone.Router.prototype.initialize.call(this); Backbone.Router.prototype.initialize.call(this);
_.bindAll(this, 'allThreads', 'showThread'); _.bindAll(this, 'allThreads', 'showThread');
this.courseId = options.courseId; this.rootUrl = options.rootUrl;
this.discussion = options.discussion; this.discussion = options.discussion;
this.course_settings = options.courseSettings; this.courseSettings = options.courseSettings;
this.discussionBoardView = options.discussionBoardView; this.discussionBoardView = options.discussionBoardView;
this.newPostView = options.newPostView; this.newPostView = options.newPostView;
}, },
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
Backbone.history.start({ Backbone.history.start({
pushState: true, pushState: true,
root: '/courses/' + this.courseId + '/discussion/forum/' root: this.rootUrl
}); });
}, },
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
el: $('.forum-content'), el: $('.forum-content'),
model: this.thread, model: this.thread,
mode: 'tab', mode: 'tab',
course_settings: this.course_settings courseSettings: this.courseSettings
}); });
this.main.render(); this.main.render();
this.main.on('thread:responses:rendered', function() { this.main.on('thread:responses:rendered', function() {
......
...@@ -33,14 +33,14 @@ define( ...@@ -33,14 +33,14 @@ define(
DiscussionBoardFactory({ DiscussionBoardFactory({
el: $('#discussion-container'), el: $('#discussion-container'),
courseId: 'test_course_id', courseId: 'test_course_id',
course_name: 'Test Course', courseName: 'Test Course',
user_info: DiscussionSpecHelper.getTestUserInfo(), user_info: DiscussionSpecHelper.getTestUserInfo(),
roles: DiscussionSpecHelper.getTestRoleInfo(), roles: DiscussionSpecHelper.getTestRoleInfo(),
sort_preference: null, sortPreference: null,
threads: [], threads: [],
thread_pages: [], thread_pages: [],
content_info: null, contentInfo: null,
course_settings: { courseSettings: {
is_cohorted: false, is_cohorted: false,
allow_anonymous: false, allow_anonymous: false,
allow_anonymous_to_peers: false, allow_anonymous_to_peers: false,
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
el: this.$('.forum-content'), el: this.$('.forum-content'),
model: thread, model: thread,
mode: 'inline', mode: 'inline',
course_settings: this.courseSettings courseSettings: this.courseSettings
}); });
this.threadView.render(); this.threadView.render();
this.listenTo(this.threadView.showView, 'thread:_delete', this.navigateToAllThreads); this.listenTo(this.threadView.showView, 'thread:_delete', this.navigateToAllThreads);
......
## mako ## mako
<%! main_css = "style-discussion-main" %> <%namespace name='static' file='../static_content.html'/>
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%def name="online_help_token()"><% return "discussions" %></%def>
<%! <%!
import json import json
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -14,42 +12,11 @@ from django.core.urlresolvers import reverse ...@@ -14,42 +12,11 @@ from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML
%> %>
<%block name="bodyclass">discussion</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra">
<%include file="../discussion/_js_head_dependencies.html" />
</%block>
<%block name="base_js_dependencies">
## Enable fast preview to fix discussion MathJax rendering bug when page first loads.
<%include file="/discussion/_js_body_dependencies.html" args="disable_fast_preview=False"/>
</%block>
<%block name="js_extra">
<%static:require_module module_name="discussion/js/discussion_board_factory" class_name="DiscussionBoardFactory">
DiscussionBoardFactory({
courseId: '${unicode(course.id) | n, js_escaped_string}',
$el: $(".discussion-board"),
user_info: ${user_info | n, dump_js_escaped_json},
roles: ${roles | n, dump_js_escaped_json},
sort_preference: '${sort_preference | n, js_escaped_string}',
threads: ${threads | n, dump_js_escaped_json},
thread_pages: '${thread_pages | n, js_escaped_string}',
content_info: ${annotated_content_info | n, dump_js_escaped_json},
course_name: '${course.display_name_with_default | n, js_escaped_string}',
course_settings: ${course_settings | n, dump_js_escaped_json}
});
</%static:require_module>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<%block name="content">
<section class="discussion discussion-board container" id="discussion-container" <section class="discussion discussion-board container" id="discussion-container"
data-course-id="${course_id}" data-course-id="${course.id}"
data-user-create-comment="${json.dumps(can_create_comment)}" data-user-create-comment="${json.dumps(can_create_comment)}"
data-user-create-subcomment="${json.dumps(can_create_subcomment)}" data-user-create-subcomment="${json.dumps(can_create_subcomment)}"
data-read-only="false" data-read-only="false"
...@@ -88,7 +55,6 @@ DiscussionBoardFactory({ ...@@ -88,7 +55,6 @@ DiscussionBoardFactory({
</div> </div>
</div> </div>
</section> </section>
</%block>
<%include file="_underscore_templates.html" /> <%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" /> <%include file="_thread_list_template.html" />
## mako
<%!
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
%>
(function (require, define) {
var registerDiscussionClass = function(moduleName, modulePath) {
define(
modulePath,
[],
function() {
var discussionClass = window[moduleName];
if (!discussionClass) {
throw new Error('Discussion class not loaded: ' + moduleName);
}
return discussionClass;
}
);
}
## Add RequireJS definitions for each discussion class
<%
discussion_classes = [
['Discussion', 'common/js/discussion/discussion'],
['Content', 'common/js/discussion/content'],
['DiscussionModuleView', 'common/js/discussion/discussion_module_view'],
['DiscussionThreadView', 'common/js/discussion/views/discussion_thread_view'],
['DiscussionThreadListView', 'common/js/discussion/views/discussion_thread_list_view'],
['DiscussionThreadProfileView', 'common/js/discussion/views/discussion_thread_profile_view'],
['DiscussionUtil', 'common/js/discussion/utils'],
['DiscussionCourseSettings', 'common/js/discussion/models/discussion_course_settings'],
['DiscussionUser', 'common/js/discussion/models/discussion_user'],
['NewPostView', 'common/js/discussion/views/new_post_view'],
]
%>
% for discussion_class_info in discussion_classes:
registerDiscussionClass(
'${discussion_class_info[0] | n, js_escaped_string}',
'${discussion_class_info[1] | n, js_escaped_string}'
);
% endfor
## Install the discussion board once the DOM is ready
$(function() {
require(['discussion/js/discussion_board_factory'], function (DiscussionBoardFactory) {
DiscussionBoardFactory({
courseId: '${unicode(course.id) | n, js_escaped_string}',
$el: $(".discussion-board"),
rootUrl: '${root_url | n, js_escaped_string}',
userInfo: ${user_info | n, dump_js_escaped_json},
roles: ${roles | n, dump_js_escaped_json},
sortPreference: '${sort_preference | n, js_escaped_string}',
threads: ${threads | n, dump_js_escaped_json},
threadPages: '${thread_pages | n, js_escaped_string}',
contentInfo: ${annotated_content_info | n, dump_js_escaped_json},
courseName: '${course.display_name_with_default | n, js_escaped_string}',
courseSettings: ${course_settings | n, dump_js_escaped_json}
});
});
});
}).call(this, require || RequireJS.require, define || RequireJS.define);
## mako ## mako
<%! main_css = "style-discussion-main" %>
<%! from django.utils.translation import ugettext as _ %>
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%inherit file="../main.html" />
<%block name="bodyclass">discussion</%block>
<%block name="headextra"> <%!
<%include file="../discussion/_js_head_dependencies.html" /> from django.utils.translation import ugettext as _
</%block> %>
<%block name="content"> <%block name="content">
<h2>${_("Discussion unavailable")}</h2> <h2>${_("Discussion unavailable")}</h2>
......
...@@ -356,11 +356,11 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase): ...@@ -356,11 +356,11 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# course is outside the context manager that is verifying the number of queries, # course is outside the context manager that is verifying the number of queries,
# and with split mongo, that method ends up querying disabled_xblocks (which is then # and with split mongo, that method ends up querying disabled_xblocks (which is then
# cached and hence not queried as part of call_single_thread). # cached and hence not queried as part of call_single_thread).
(ModuleStoreEnum.Type.mongo, 1, 6, 4, 15, 3), (ModuleStoreEnum.Type.mongo, 1, 5, 3, 13, 1),
(ModuleStoreEnum.Type.mongo, 50, 6, 4, 15, 3), (ModuleStoreEnum.Type.mongo, 50, 5, 3, 13, 1),
# split mongo: 3 queries, regardless of thread response size. # split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, 1, 3, 3, 14, 3), (ModuleStoreEnum.Type.split, 1, 3, 3, 12, 1),
(ModuleStoreEnum.Type.split, 50, 3, 3, 14, 3), (ModuleStoreEnum.Type.split, 50, 3, 3, 12, 1),
) )
@ddt.unpack @ddt.unpack
def test_number_of_mongo_queries( def test_number_of_mongo_queries(
......
...@@ -3,6 +3,8 @@ Forum urls for the django_comment_client. ...@@ -3,6 +3,8 @@ Forum urls for the django_comment_client.
""" """
from django.conf.urls import url, patterns from django.conf.urls import url, patterns
from .views import DiscussionBoardFragmentView
urlpatterns = patterns( urlpatterns = patterns(
'discussion.views', 'discussion.views',
...@@ -10,5 +12,10 @@ urlpatterns = patterns( ...@@ -10,5 +12,10 @@ urlpatterns = patterns(
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'), url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
url(r'^(?P<discussion_id>[\w\-.]+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'), url(r'^(?P<discussion_id>[\w\-.]+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
url(r'^(?P<discussion_id>[\w\-.]+)/inline$', 'inline_discussion', name='inline_discussion'), url(r'^(?P<discussion_id>[\w\-.]+)/inline$', 'inline_discussion', name='inline_discussion'),
url(
r'discussion_board_fragment_view$',
DiscussionBoardFragmentView.as_view(),
name='discussion_board_fragment_view'
),
url(r'', 'forum_form_discussion', name='forum_form_discussion'), url(r'', 'forum_form_discussion', name='forum_form_discussion'),
) )
...@@ -8,7 +8,7 @@ import urlparse ...@@ -8,7 +8,7 @@ import urlparse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import exceptions from django.core import exceptions
from django.http import Http404, HttpResponseBadRequest, HttpResponse from django.http import Http404, HttpResponse, HttpResponseServerError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators import csrf from django.views.decorators import csrf
from django.views.decorators.http import require_GET, require_POST from django.views.decorators.http import require_GET, require_POST
...@@ -243,7 +243,7 @@ def create_thread(request, course_id, commentable_id): ...@@ -243,7 +243,7 @@ def create_thread(request, course_id, commentable_id):
try: try:
group_id = get_group_id_for_comments_service(request, course_key, commentable_id) group_id = get_group_id_for_comments_service(request, course_key, commentable_id)
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid cohort id") return HttpResponseServerError("Invalid cohort id")
if group_id is not None: if group_id is not None:
thread.group_id = group_id thread.group_id = group_id
......
...@@ -94,8 +94,11 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin): ...@@ -94,8 +94,11 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request): def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
invalid_id = self.student_cohort.id + self.moderator_cohort.id invalid_id = self.student_cohort.id + self.moderator_cohort.id
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) try:
self.assertEqual(response.status_code, 400) response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
self.assertEqual(response.status_code, 500)
except ValueError:
pass # In mock request mode, server errors are not captured
class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin): class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
......
...@@ -544,6 +544,9 @@ TEMPLATES = [ ...@@ -544,6 +544,9 @@ TEMPLATES = [
] ]
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0] DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
# The template used to render a web fragment as a standalone page
STANDALONE_FRAGMENT_VIEW_TEMPLATE = 'fragment-view-chromeless.html'
############################################################################################### ###############################################################################################
# use the ratelimit backend to prevent brute force attacks # use the ratelimit backend to prevent brute force attacks
...@@ -1927,6 +1930,10 @@ INSTALLED_APPS = ( ...@@ -1927,6 +1930,10 @@ INSTALLED_APPS = (
'pipeline', 'pipeline',
'static_replace', 'static_replace',
# For user interface plugins
'web_fragments',
'openedx.core.djangoapps.plugin_api',
# For content serving # For content serving
'openedx.core.djangoapps.contentserver', 'openedx.core.djangoapps.contentserver',
......
## mako
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<% <%
tech_support_email='<a href=\"mailto:{tech_support_email}\">{tech_support_email}</a>'.format(tech_support_email=settings.TECH_SUPPORT_EMAIL) tech_support_email='<a href=\"mailto:{tech_support_email}\">{tech_support_email}</a>'.format(tech_support_email=settings.TECH_SUPPORT_EMAIL)
......
## mako ## mako
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%! <%!
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
...@@ -11,19 +9,18 @@ from openedx.core.djangolib.markup import HTML ...@@ -11,19 +9,18 @@ from openedx.core.djangolib.markup import HTML
<%block name="bodyclass">view-in-course view-statictab ${course.css_class or ''}</%block> <%block name="bodyclass">view-in-course view-statictab ${course.css_class or ''}</%block>
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="headextra"> <%block name="head_extra">
<%static:css group='style-course-vendor'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
${HTML(fragment.head_html())} ${HTML(fragment.head_html())}
</%block> </%block>
<%block name="js_extra">
<%block name="footer_extra">
<%include file="/mathjax_include.html" args="disable_fast_preview=True"/> <%include file="/mathjax_include.html" args="disable_fast_preview=True"/>
${HTML(fragment.foot_html())} ${HTML(fragment.foot_html())}
</%block> </%block>
<%block name="pagetitle">${tab['name']} | ${course.display_number_with_default}</%block>
<%block name="pagetitle">${tab['name']} | ${course.display_number_with_default | h}</%block>
<%include file="/courseware/course_navigation.html" args="active_page=active_page" /> <%include file="/courseware/course_navigation.html" args="active_page=active_page" />
......
## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%!
from openedx.core.djangolib.markup import HTML
%>
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">${tab['body_class']}</%block>
<%def name="online_help_token()"><% return "${tab['online_help_token']}" %></%def>
<%block name="pagetitle">${tab['name']} | ${course.display_number_with_default}</%block>
<%include file="/courseware/course_navigation.html" args="active_page=active_page" />
<%block name="head_extra">
${HTML(fragment.head_html())}
</%block>
<%block name="footer_extra">
<%include file="/mathjax_include.html" args="disable_fast_preview=True"/>
${HTML(fragment.foot_html())}
</%block>
<%block name="content">
${HTML(fragment.body_html())}
</%block>
## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%inherit file="/main.html" />
<%namespace name='static' file='static_content.html'/>
<%! from openedx.core.djangolib.markup import HTML %>
<% header_file = None %>
<%block name="head_extra">
${HTML(fragment.head_html())}
</%block>
<%block name="footer_extra">
${HTML(fragment.foot_html())}
</%block>
<div class="content-wrapper" id="container">
${HTML(fragment.body_html())}
</div>
...@@ -96,6 +96,7 @@ from pipeline_mako import render_require_js_path_overrides ...@@ -96,6 +96,7 @@ from pipeline_mako import render_require_js_path_overrides
% endif % endif
<%block name="headextra"/> <%block name="headextra"/>
<%block name="head_extra"/>
<%static:optional_include_mako file="head-extra.html" is_theming_enabled="True" /> <%static:optional_include_mako file="head-extra.html" is_theming_enabled="True" />
...@@ -148,6 +149,7 @@ from pipeline_mako import render_require_js_path_overrides ...@@ -148,6 +149,7 @@ from pipeline_mako import render_require_js_path_overrides
</div> </div>
% endif % endif
<%block name="footer_extra"/>
<%block name="js_extra"/> <%block name="js_extra"/>
<%include file="widgets/segment-io-footer.html" /> <%include file="widgets/segment-io-footer.html" />
......
...@@ -8,7 +8,7 @@ from django.views.generic.base import RedirectView ...@@ -8,7 +8,7 @@ from django.views.generic.base import RedirectView
from ratelimitbackend import admin from ratelimitbackend import admin
from django.conf.urls.static import static from django.conf.urls.static import static
from courseware.views.views import EnrollStaffView from courseware.views.views import CourseTabView, EnrollStaffView, StaticCourseTabView
from config_models.views import ConfigurationModelCurrentAPIView from config_models.views import ConfigurationModelCurrentAPIView
from courseware.views.index import CoursewareIndex from courseware.views.index import CoursewareIndex
from openedx.core.djangoapps.auth_exchange.views import LoginWithAccessTokenView from openedx.core.djangoapps.auth_exchange.views import LoginWithAccessTokenView
...@@ -691,8 +691,8 @@ urlpatterns += ( ...@@ -691,8 +691,8 @@ urlpatterns += (
r'^courses/{}/tab/(?P<tab_type>[^/]+)/$'.format( r'^courses/{}/tab/(?P<tab_type>[^/]+)/$'.format(
settings.COURSE_ID_PATTERN, settings.COURSE_ID_PATTERN,
), ),
'courseware.views.views.content_tab', CourseTabView.as_view(),
name='content_tab', name='course_tab_view',
), ),
) )
...@@ -702,7 +702,7 @@ urlpatterns += ( ...@@ -702,7 +702,7 @@ urlpatterns += (
r'^courses/{}/(?P<tab_slug>[^/]+)/$'.format( r'^courses/{}/(?P<tab_slug>[^/]+)/$'.format(
settings.COURSE_ID_PATTERN, settings.COURSE_ID_PATTERN,
), ),
'courseware.views.views.static_tab', StaticCourseTabView.as_view(),
name='static_tab', name='static_tab',
), ),
) )
......
"""
Views for building plugins.
"""
from abc import abstractmethod
import logging
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.shortcuts import render_to_response
from web_fragments.views import FragmentView
log = logging.getLogger('plugin_api')
class EdxFragmentView(FragmentView):
"""
The base class of all Open edX fragment views.
"""
USES_PATTERN_LIBRARY = True
page_title = None
@staticmethod
def get_css_dependencies(group):
"""
Returns list of CSS dependencies belonging to `group` in settings.PIPELINE_JS.
Respects `PIPELINE_ENABLED` setting.
"""
if settings.PIPELINE_ENABLED:
return [settings.PIPELINE_CSS[group]['output_filename']]
else:
return settings.PIPELINE_CSS[group]['source_filenames']
@staticmethod
def get_js_dependencies(group):
"""
Returns list of JS dependencies belonging to `group` in settings.PIPELINE_JS.
Respects `PIPELINE_ENABLED` setting.
"""
if settings.PIPELINE_ENABLED:
return [settings.PIPELINE_JS[group]['output_filename']]
else:
return settings.PIPELINE_JS[group]['source_filenames']
@abstractmethod
def vendor_js_dependencies(self):
"""
Returns list of the vendor JS files that this view depends on.
"""
return []
@abstractmethod
def js_dependencies(self):
"""
Returns list of the JavaScript files that this view depends on.
"""
return []
@abstractmethod
def css_dependencies(self):
"""
Returns list of the CSS files that this view depends on.
"""
return []
def add_fragment_resource_urls(self, fragment):
"""
Adds URLs for JS and CSS resources needed by this fragment.
"""
# Head dependencies
for vendor_js_file in self.vendor_js_dependencies():
fragment.add_resource_url(staticfiles_storage.url(vendor_js_file), 'application/javascript', 'head')
for css_file in self.css_dependencies():
fragment.add_css_url(staticfiles_storage.url(css_file))
# Body dependencies
for js_file in self.js_dependencies():
fragment.add_javascript_url(staticfiles_storage.url(js_file))
def render_to_standalone_html(self, request, fragment, **kwargs):
"""
Renders this fragment to HTML for a standalone page.
"""
context = {
'uses-pattern-library': self.USES_PATTERN_LIBRARY,
'settings': settings,
'fragment': fragment,
'disable_accordion': True,
'allow_iframing': True,
'disable_header': True,
'disable_footer': True,
'disable_window_wrap': True,
'disable_preview_menu': True,
}
return render_to_response(settings.STANDALONE_FRAGMENT_VIEW_TEMPLATE, context)
...@@ -10,9 +10,9 @@ from openedx.core.lib.course_tabs import CourseTabPluginManager ...@@ -10,9 +10,9 @@ from openedx.core.lib.course_tabs import CourseTabPluginManager
@attr(shard=2) @attr(shard=2)
class TestPluginApi(TestCase): class TestCourseTabApi(TestCase):
""" """
Unit tests for the plugin API Unit tests for the course tab plugin API
""" """
def test_get_plugin(self): def test_get_plugin(self):
......
...@@ -206,5 +206,9 @@ py2neo==3.1.2 ...@@ -206,5 +206,9 @@ py2neo==3.1.2
# for calculating coverage # for calculating coverage
-r coverage.txt -r coverage.txt
# Support for plugins
web-fragments==0.2.1
xblock==0.4.14
# Third Party XBlocks # Third Party XBlocks
edx-sga==0.6.2 edx-sga==0.6.2
...@@ -71,7 +71,6 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx ...@@ -71,7 +71,6 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx
git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
# Our libraries: # Our libraries:
git+https://github.com/edx/XBlock.git@xblock-0.4.13#egg=XBlock==0.4.13
-e git+https://github.com/edx/codejail.git@a320d43ce6b9c93b17636b2491f724d9e433be47#egg=codejail==0.0 -e git+https://github.com/edx/codejail.git@a320d43ce6b9c93b17636b2491f724d9e433be47#egg=codejail==0.0
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
......
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