Commit f6876ce1 by Andy Armstrong Committed by GitHub

Merge pull request #13937 from edx/andya/use-ui-blocks

[WIP] Use web fragments for discussion and static tabs
parents 77a29242 248558f1
......@@ -119,7 +119,7 @@ source, template_path = Loader(engine).load_template_source(path)
}).call(this, require || RequireJS.require);
% else:
## 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
## 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 () {
......
......@@ -7,12 +7,17 @@ import logging
from xblock.fields import List
from openedx.core.lib.api.plugins import PluginError
from django.core.files.storage import get_storage_class
log = logging.getLogger("edx.courseware")
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text
# A list of attributes on course tabs that can not be updated
READ_ONLY_COURSE_TAB_ATTRIBUTES = ['type']
class CourseTab(object):
"""
......@@ -31,6 +36,12 @@ class CourseTab(object):
# ugettext_noop since the user won't be available in this context.
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
is_hideable = False
......@@ -68,14 +79,25 @@ class CourseTab(object):
Args:
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.tab_id = tab_dict.get('tab_id', getattr(self, 'tab_id', self.type))
self.link_func = tab_dict.get('link_func', link_reverse_func(self.view_name))
self.course_staff_only = tab_dict.get('course_staff_only', False)
self.is_hidden = tab_dict.get('is_hidden', False)
self.tab_dict = tab_dict
@property
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))
@classmethod
def is_enabled(cls, course, user=None):
"""Returns true if this course tab is enabled in the course.
......@@ -101,16 +123,8 @@ class CourseTab(object):
This method allows callers to access CourseTab members with the d[key] syntax as is done with
Python dictionary objects.
"""
if key == 'name':
return self.name
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
if hasattr(self, key):
return getattr(self, key, None)
else:
raise KeyError('Key {0} not present in tab {1}'.format(key, self.to_json()))
......@@ -121,14 +135,8 @@ class CourseTab(object):
Note: the 'type' member can be 'get', but not 'set'.
"""
if key == 'name':
self.name = 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
if hasattr(self, key) and key not in READ_ONLY_COURSE_TAB_ATTRIBUTES:
setattr(self, key, value)
else:
raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json()))
......@@ -230,6 +238,54 @@ class CourseTab(object):
return tab_type(tab_dict=tab_dict)
class TabFragmentViewMixin(object):
"""
A mixin for tabs that render themselves as web fragments.
"""
fragment_view_name = None
def __init__(self, tab_dict):
super(TabFragmentViewMixin, self).__init__(tab_dict)
self._fragment_view = None
@property
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):
""" Returns a function that returns the course tab's URL. """
return reverse_func("course_tab_view", args=[course.id.to_deprecated_string(), self.type])
return link_func
@property
def url_slug(self):
"""
Returns the slug to be included in this tab's URL.
"""
return "tab/" + self.type
@property
def fragment_view(self):
"""
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):
"""
A custom tab.
......@@ -240,7 +296,7 @@ class StaticTab(CourseTab):
def __init__(self, tab_dict=None, name=None, url_slug=None):
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])
self.url_slug = tab_dict.get('url_slug') if tab_dict else url_slug
......
......@@ -9,7 +9,6 @@ from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager
from student.models import CourseEnrollment
from student.roles import CourseStaffRole
from xmodule.tabs import CourseTab, CourseTabList, key_checker
......
......@@ -14,7 +14,7 @@ from courseware.tabs import (
)
from courseware.tests.helpers import LoginEnrollmentTestCase
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 student.models import CourseEnrollment
from student.tests.factories import UserFactory
......@@ -258,16 +258,16 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
self.setup_user()
request = get_mock_request(self.user)
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()
course = get_course_by_id(self.course.id)
request = get_mock_request(self.user)
tab = xmodule_tabs.CourseTabList.get_tab_by_slug(course.tabs, 'new_tab')
# 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('static_tab', tab_content)
......@@ -276,8 +276,8 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
mock_module_render.return_value = MagicMock(
render=Mock(side_effect=Exception('Render failed!'))
)
static_tab = get_static_tab_contents(request, course, tab)
self.assertIn("this module is temporarily unavailable", static_tab)
static_tab_content = get_static_tab_fragment(request, course, tab).content
self.assertIn("this module is temporarily unavailable", static_tab_content)
@attr(shard=1)
......
......@@ -13,11 +13,18 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.core.context_processors import csrf
from django.db import transaction
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, QueryDict
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
QueryDict,
)
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.timezone import UTC
......@@ -31,7 +38,6 @@ from ipware.ip import get_ip
from markupsafe import escape
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from rest_framework import status
from lms.djangoapps.instructor.views.api import require_global_staff
from lms.djangoapps.ccx.utils import prep_course_for_grading
......@@ -98,6 +104,9 @@ from xmodule.x_module import STUDENT_VIEW
from ..entrance_exams import user_must_complete_entrance_exam
from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
from web_fragments.fragment import Fragment
from web_fragments.views import FragmentView
log = logging.getLogger("edx.courseware")
......@@ -228,7 +237,7 @@ def jump_to_id(request, course_id, module_id):
This entry point allows for a shorter version of a jump to where just the id of the element is
passed in. This assumes that id is unique within the course_id namespace
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
items = modulestore().get_items(course_key, qualifiers={'name': module_id})
if len(items) == 0:
......@@ -439,36 +448,79 @@ def get_last_accessed_courseware(course, request, user):
return None
@ensure_csrf_cookie
@ensure_valid_course_key
def static_tab(request, course_id, tab_slug):
class StaticCourseTabView(FragmentView):
"""
Display the courses tab with the given name.
Assumes the course_id is in a valid format.
View that displays a static course tab with a given name.
"""
@method_decorator(ensure_csrf_cookie)
@method_decorator(ensure_valid_course_key)
def get(self, request, course_id, tab_slug, **kwargs):
"""
Displays a static course tab page with a given name
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
if tab is None:
raise Http404
return super(StaticCourseTabView, self).get(request, course=course, tab=tab, **kwargs)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
"""
Renders the static tab to a fragment.
"""
return get_static_tab_fragment(request, course, tab)
course = get_course_with_access(request.user, 'load', course_key)
def render_to_standalone_html(self, request, fragment, course=None, tab=None, **kwargs):
"""
Renders this static tab's fragment to HTML for a standalone page.
"""
return render_to_response('courseware/static_tab.html', {
'course': course,
'active_page': 'static_tab_{0}'.format(tab['url_slug']),
'tab': tab,
'fragment': fragment,
'uses_pattern_library': False,
'disable_courseware_js': True,
})
tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
if tab is None:
raise Http404
contents = get_static_tab_contents(
request,
course,
tab
)
if contents is None:
raise Http404
class CourseTabView(FragmentView):
"""
View that displays a course tab page.
"""
@method_decorator(ensure_csrf_cookie)
@method_decorator(ensure_valid_course_key)
def get(self, request, course_id, tab_type, **kwargs):
"""
Displays a course tab page that contains a web fragment.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
tab = CourseTabList.get_tab_by_type(course.tabs, tab_type)
return super(CourseTabView, self).get(request, course=course, tab=tab, **kwargs)
return render_to_response('courseware/static_tab.html', {
'course': course,
'tab': tab,
'tab_contents': contents,
})
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
"""
Renders the course tab to a fragment.
"""
return tab.render_to_fragment(request, course, **kwargs)
def render_to_standalone_html(self, request, fragment, course=None, tab=None, **kwargs):
"""
Renders this course tab's fragment to HTML for a standalone page.
"""
return render_to_string(
'courseware/tab-view.html',
{
'course': course,
'active_page': tab['type'],
'tab': tab,
'fragment': fragment,
'uses_pattern_library': True,
'disable_courseware_js': True,
},
)
@ensure_csrf_cookie
......@@ -480,7 +532,7 @@ def syllabus(request, course_id):
Assumes the course_id is in a valid format.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
staff_access = bool(has_access(request.user, 'staff', course))
......@@ -587,7 +639,7 @@ def course_about(request, course_id):
Display the course's about page.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
if hasattr(course_key, 'ccx'):
# if un-enrolled/non-registered user try to access CCX (direct for registration)
......@@ -990,7 +1042,7 @@ def submission_history(request, course_id, student_username, location):
StudentModuleHistory records.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
try:
usage_key = course_key.make_usage_key_from_deprecated_string(location)
......@@ -1055,9 +1107,9 @@ def submission_history(request, course_id, student_username, location):
return render_to_response('courseware/submission_history.html', context)
def get_static_tab_contents(request, course, tab):
def get_static_tab_fragment(request, course, tab):
"""
Returns the contents for the given static tab
Returns the fragment for the given static tab
"""
loc = course.id.make_usage_key(
tab.type,
......@@ -1072,17 +1124,17 @@ def get_static_tab_contents(request, course, tab):
logging.debug('course_module = %s', tab_module)
html = ''
fragment = Fragment()
if tab_module is not None:
try:
html = tab_module.render(STUDENT_VIEW).content
fragment = tab_module.render(STUDENT_VIEW, {})
except Exception: # pylint: disable=broad-except
html = render_to_string('courseware/error-message.html', None)
fragment.content = render_to_string('courseware/error-message.html', None)
log.exception(
u"Error rendering course=%s, tab=%s", course, tab['url_slug']
)
return html
return fragment
@require_GET
......@@ -1104,7 +1156,7 @@ def get_course_lti_endpoints(request, course_id):
(django response object): HTTP response. 404 if course is not found, otherwise 200 with JSON body.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
try:
course = get_course(course_key, depth=2)
......@@ -1153,7 +1205,7 @@ def course_survey(request, course_id):
views.py file in the Survey Djangoapp
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
redirect_url = reverse('info', args=[course_id])
......
......@@ -7,9 +7,10 @@ from django.utils.translation import ugettext_noop
from courseware.tabs import EnrolledTab
import django_comment_client.utils as utils
from xmodule.tabs import TabFragmentViewMixin
class DiscussionTab(EnrolledTab):
class DiscussionTab(TabFragmentViewMixin, EnrolledTab):
"""
A tab for the cs_comments_service forums.
"""
......@@ -18,8 +19,11 @@ class DiscussionTab(EnrolledTab):
title = ugettext_noop('Discussion')
priority = None
view_name = 'discussion.views.forum_form_discussion'
fragment_view_name = 'discussion.views.DiscussionBoardFragmentView'
is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
is_default = False
body_class = 'discussion'
online_help_token = 'discussions'
@classmethod
def is_enabled(cls, course, user=None):
......
......@@ -17,11 +17,11 @@
function($, Backbone, Content, Discussion, DiscussionUtil, DiscussionCourseSettings, DiscussionUser,
NewPostView, DiscussionRouter, DiscussionBoardView) {
return function(options) {
var userInfo = options.user_info,
sortPreference = options.sort_preference,
var userInfo = options.userInfo,
sortPreference = options.sortPreference,
threads = options.threads,
threadPages = options.thread_pages,
contentInfo = options.content_info,
threadPages = options.threadPages,
contentInfo = options.contentInfo,
user = new DiscussionUser(userInfo),
discussion,
courseSettings,
......@@ -33,14 +33,14 @@
// TODO: eliminate usage of global variables when possible
DiscussionUtil.loadRoles(options.roles);
window.$$course_id = options.courseId;
window.courseName = options.course_name;
window.courseName = options.courseName;
DiscussionUtil.setUser(user);
window.user = user;
Content.loadContentInfos(contentInfo);
// Create a discussion model
discussion = new Discussion(threads, {pages: threadPages, sort: sortPreference});
courseSettings = new DiscussionCourseSettings(options.course_settings);
courseSettings = new DiscussionCourseSettings(options.courseSettings);
// Create the discussion board view
discussionBoardView = new DiscussionBoardView({
......@@ -61,7 +61,7 @@
// Set up a router to manage the page's history
router = new DiscussionRouter({
courseId: options.courseId,
rootUrl: options.rootUrl,
discussion: discussion,
courseSettings: courseSettings,
discussionBoardView: discussionBoardView,
......
......@@ -35,7 +35,7 @@
DiscussionUtil.loadRoles(options.roles);
window.$$course_id = options.courseId;
window.courseName = options.course_name;
window.courseName = options.courseName;
DiscussionUtil.setUser(user);
window.user = user;
Content.loadContentInfos(contentInfo);
......
......@@ -18,9 +18,9 @@
initialize: function(options) {
Backbone.Router.prototype.initialize.call(this);
_.bindAll(this, 'allThreads', 'showThread');
this.courseId = options.courseId;
this.rootUrl = options.rootUrl;
this.discussion = options.discussion;
this.course_settings = options.courseSettings;
this.courseSettings = options.courseSettings;
this.discussionBoardView = options.discussionBoardView;
this.newPostView = options.newPostView;
},
......@@ -50,7 +50,7 @@
Backbone.history.start({
pushState: true,
root: '/courses/' + this.courseId + '/discussion/forum/'
root: this.rootUrl
});
},
......@@ -95,7 +95,7 @@
el: $('.forum-content'),
model: this.thread,
mode: 'tab',
course_settings: this.course_settings
courseSettings: this.courseSettings
});
this.main.render();
this.main.on('thread:responses:rendered', function() {
......
......@@ -33,14 +33,14 @@ define(
DiscussionBoardFactory({
el: $('#discussion-container'),
courseId: 'test_course_id',
course_name: 'Test Course',
courseName: 'Test Course',
user_info: DiscussionSpecHelper.getTestUserInfo(),
roles: DiscussionSpecHelper.getTestRoleInfo(),
sort_preference: null,
sortPreference: null,
threads: [],
thread_pages: [],
content_info: null,
course_settings: {
contentInfo: null,
courseSettings: {
is_cohorted: false,
allow_anonymous: false,
allow_anonymous_to_peers: false,
......
......@@ -56,7 +56,7 @@
el: this.$('.forum-content'),
model: thread,
mode: 'inline',
course_settings: this.courseSettings
courseSettings: this.courseSettings
});
this.threadView.render();
this.listenTo(this.threadView.showView, 'thread:_delete', this.navigateToAllThreads);
......
## mako
<%! main_css = "style-discussion-main" %>
<%namespace name='static' file='../static_content.html'/>
<%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
from django.utils.translation import ugettext as _
......@@ -14,42 +12,11 @@ from django.core.urlresolvers import reverse
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.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"
data-course-id="${course_id}"
data-course-id="${course.id}"
data-user-create-comment="${json.dumps(can_create_comment)}"
data-user-create-subcomment="${json.dumps(can_create_subcomment)}"
data-read-only="false"
......@@ -88,7 +55,6 @@ DiscussionBoardFactory({
</div>
</div>
</section>
</%block>
<%include file="_underscore_templates.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
<%! main_css = "style-discussion-main" %>
<%! from django.utils.translation import ugettext as _ %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%block name="bodyclass">discussion</%block>
<%block name="headextra">
<%include file="../discussion/_js_head_dependencies.html" />
</%block>
<%!
from django.utils.translation import ugettext as _
%>
<%block name="content">
<h2>${_("Discussion unavailable")}</h2>
......
......@@ -356,11 +356,11 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# 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
# cached and hence not queried as part of call_single_thread).
(ModuleStoreEnum.Type.mongo, 1, 6, 4, 15, 3),
(ModuleStoreEnum.Type.mongo, 50, 6, 4, 15, 3),
(ModuleStoreEnum.Type.mongo, 1, 5, 3, 13, 1),
(ModuleStoreEnum.Type.mongo, 50, 5, 3, 13, 1),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, 1, 3, 3, 14, 3),
(ModuleStoreEnum.Type.split, 50, 3, 3, 14, 3),
(ModuleStoreEnum.Type.split, 1, 3, 3, 12, 1),
(ModuleStoreEnum.Type.split, 50, 3, 3, 12, 1),
)
@ddt.unpack
def test_number_of_mongo_queries(
......
......@@ -3,6 +3,8 @@ Forum urls for the django_comment_client.
"""
from django.conf.urls import url, patterns
from .views import DiscussionBoardFragmentView
urlpatterns = patterns(
'discussion.views',
......@@ -10,5 +12,10 @@ urlpatterns = patterns(
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\-.]+)/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'),
)
......@@ -8,7 +8,7 @@ import urlparse
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
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.views.decorators import csrf
from django.views.decorators.http import require_GET, require_POST
......@@ -243,7 +243,7 @@ def create_thread(request, course_id, commentable_id):
try:
group_id = get_group_id_for_comments_service(request, course_key, commentable_id)
except ValueError:
return HttpResponseBadRequest("Invalid cohort id")
return HttpResponseServerError("Invalid cohort id")
if group_id is not None:
thread.group_id = group_id
......
......@@ -94,8 +94,11 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
invalid_id = self.student_cohort.id + self.moderator_cohort.id
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
self.assertEqual(response.status_code, 400)
try:
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):
......
......@@ -544,6 +544,9 @@ TEMPLATES = [
]
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
......@@ -1932,6 +1935,10 @@ INSTALLED_APPS = (
'pipeline',
'static_replace',
# For user interface plugins
'web_fragments',
'openedx.core.djangoapps.plugin_api',
# For content serving
'openedx.core.djangoapps.contentserver',
......
## mako
<%! 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)
......
## mako
<%page expression_filter="h"/>
<%!
from openedx.core.djangolib.markup import HTML
%>
<%inherit file="/main.html" />
<%block name="bodyclass">view-in-course view-statictab ${course.css_class or ''}</%block>
<%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'/>
${HTML(fragment.head_html())}
</%block>
<%block name="js_extra">
<%block name="footer_extra">
<%include file="/mathjax_include.html" args="disable_fast_preview=True"/>
${HTML(fragment.foot_html())}
</%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='static_tab_{0}'.format(tab['url_slug'])" />
<%include file="/courseware/course_navigation.html" args="active_page=active_page" />
<main id="main" aria-label="Content" tabindex="-1">
<section class="container">
<div class="static_tab_wrapper">
${tab_contents}
${HTML(fragment.body_html())}
</div>
</section>
</main>
## 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
% endif
<%block name="headextra"/>
<%block name="head_extra"/>
<%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
</div>
% endif
<%block name="footer_extra"/>
<%block name="js_extra"/>
<%include file="widgets/segment-io-footer.html" />
......
......@@ -8,7 +8,7 @@ from django.views.generic.base import RedirectView
from ratelimitbackend import admin
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 courseware.views.index import CoursewareIndex
from openedx.core.djangoapps.auth_exchange.views import LoginWithAccessTokenView
......@@ -685,13 +685,24 @@ if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
name='resubscribe_forum_update',
),
)
urlpatterns += (
url(
r'^courses/{}/tab/(?P<tab_type>[^/]+)/$'.format(
settings.COURSE_ID_PATTERN,
),
CourseTabView.as_view(),
name='course_tab_view',
),
)
urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.
url(
r'^courses/{}/(?P<tab_slug>[^/]+)/$'.format(
settings.COURSE_ID_PATTERN,
),
'courseware.views.views.static_tab',
StaticCourseTabView.as_view(),
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
@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):
......
......@@ -206,5 +206,9 @@ py2neo==3.1.2
# for calculating coverage
-r coverage.txt
# Support for plugins
web-fragments==0.2.1
xblock==0.4.14
# Third Party XBlocks
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
git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
# 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/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
......
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