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)
......
...@@ -18,7 +18,13 @@ from django.core.urlresolvers import reverse ...@@ -18,7 +18,13 @@ from django.core.urlresolvers import reverse
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.db import transaction from django.db import transaction
from django.db.models import Q 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.shortcuts import redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import UTC from django.utils.timezone import UTC
...@@ -32,7 +38,6 @@ from ipware.ip import get_ip ...@@ -32,7 +38,6 @@ from ipware.ip import get_ip
from markupsafe import escape from markupsafe import escape
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from rest_framework import status from rest_framework import status
from lms.djangoapps.instructor.views.api import require_global_staff from lms.djangoapps.instructor.views.api import require_global_staff
from lms.djangoapps.ccx.utils import prep_course_for_grading from lms.djangoapps.ccx.utils import prep_course_for_grading
...@@ -99,8 +104,8 @@ from xmodule.x_module import STUDENT_VIEW ...@@ -99,8 +104,8 @@ from xmodule.x_module import STUDENT_VIEW
from ..entrance_exams import user_must_complete_entrance_exam 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 ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
from web_fragments.views import FragmentView
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
from web_fragments.views import FragmentView
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
...@@ -232,7 +237,7 @@ def jump_to_id(request, course_id, module_id): ...@@ -232,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 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 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}) items = modulestore().get_items(course_key, qualifiers={'name': module_id})
if len(items) == 0: if len(items) == 0:
...@@ -443,63 +448,79 @@ def get_last_accessed_courseware(course, request, user): ...@@ -443,63 +448,79 @@ def get_last_accessed_courseware(course, request, user):
return None return None
@ensure_csrf_cookie class StaticCourseTabView(FragmentView):
@ensure_valid_course_key
def static_tab(request, course_id, tab_slug):
""" """
Display the courses tab with the given name. View that displays a static course tab with a given name.
Assumes the course_id is in a valid format.
""" """
@method_decorator(ensure_csrf_cookie)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) @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) course = get_course_with_access(request.user, 'load', course_key)
tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
if tab is None: if tab is None:
raise Http404 raise Http404
return super(StaticCourseTabView, self).get(request, course=course, tab=tab, **kwargs)
fragment = get_static_tab_fragment( def render_to_fragment(self, request, course=None, tab=None, **kwargs):
request, """
course, Renders the static tab to a fragment.
tab """
) return get_static_tab_fragment(request, course, tab)
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', { return render_to_response('courseware/static_tab.html', {
'course': course, 'course': course,
'active_page': 'static_tab_{0}'.format(tab['url_slug']), 'active_page': 'static_tab_{0}'.format(tab['url_slug']),
'tab': tab, 'tab': tab,
'fragment': fragment, 'fragment': fragment,
'uses_pattern_library': False, 'uses_pattern_library': False,
'disable_courseware_js': True 'disable_courseware_js': True,
}) })
@ensure_csrf_cookie class CourseTabView(FragmentView):
@ensure_valid_course_key
def content_tab(request, course_id, tab_type):
""" """
Display a content tab based on type name. View that displays a course tab page.
Assumes the course_id is in a valid format.
""" """
@method_decorator(ensure_csrf_cookie)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) @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) 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)
content_tab = [tab for tab in course.tabs if tab.type == tab_type][0] def render_to_fragment(self, request, course=None, tab=None, **kwargs):
fragment = content_tab.render_fragment(request, course) """
Renders the course tab to a fragment.
"""
return tab.render_to_fragment(request, course, **kwargs)
return render_to_response('courseware/static_tab.html', { 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, 'course': course,
'active_page': content_tab['type'], 'active_page': tab['type'],
'tab': content_tab, 'tab': tab,
'fragment': fragment, 'fragment': fragment,
'uses_pattern_library': True, 'uses_pattern_library': True,
'disable_courseware_js': True 'disable_courseware_js': True,
}) },
)
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -511,7 +532,7 @@ def syllabus(request, course_id): ...@@ -511,7 +532,7 @@ def syllabus(request, course_id):
Assumes the course_id is in a valid format. 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) course = get_course_with_access(request.user, 'load', course_key)
staff_access = bool(has_access(request.user, 'staff', course)) staff_access = bool(has_access(request.user, 'staff', course))
...@@ -619,7 +640,7 @@ def course_about(request, course_id): ...@@ -619,7 +640,7 @@ def course_about(request, course_id):
Assumes the course_id is in a valid format. Assumes the course_id is in a valid format.
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = CourseKey.from_string(course_id)
if hasattr(course_key, 'ccx'): if hasattr(course_key, 'ccx'):
# if un-enrolled/non-registered user try to access CCX (direct for registration) # if un-enrolled/non-registered user try to access CCX (direct for registration)
...@@ -749,65 +770,6 @@ def course_about(request, course_id): ...@@ -749,65 +770,6 @@ def course_about(request, course_id):
return render_to_response('courseware/course_about.html', context) return render_to_response('courseware/course_about.html', context)
class ProgressComponentView(FragmentView):
"""
Component implementation of the discussion board.
"""
def render_fragment(self, request, course_id=None):
"""
Render the component
"""
# nr_transaction = newrelic.agent.current_transaction()
#
course_key = CourseKey.from_string(course_id)
context = _create_progress_context(request, course_key)
html = render_to_string('discussion/discussion_board_component.html', context)
# # inline_js = render_to_string('discussion/discussion_board_js.template', context)
#
# fragment = Fragment(html)
# # fragment.add_javascript(inline_js)
fragment = Fragment()
fragment.content = "Hello World"
return fragment
def _create_progress_context(request, course_key):
course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
prep_course_for_grading(course, request)
staff_access = bool(has_access(request.user, 'staff', course))
student = request.user
# NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function.
# The pre-fetching of groups is done to make auth checks not require an
# additional DB lookup (this kills the Progress page in particular).
student = User.objects.prefetch_related("groups").get(id=student.id)
course_grade = CourseGradeFactory().create(student, course)
courseware_summary = course_grade.chapter_grades
grade_summary = course_grade.summary
studio_url = get_studio_url(course, 'settings/grading')
# checking certificate generation configuration
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(student, course_key)
context = {
'course': course,
'courseware_summary': courseware_summary,
'studio_url': studio_url,
'grade_summary': grade_summary,
'staff_access': staff_access,
'student': student,
'passed': is_course_passed(course, grade_summary),
'credit_course_requirements': _credit_course_requirements(course_key, student),
'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode)
}
return context
@transaction.non_atomic_requests @transaction.non_atomic_requests
@login_required @login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -1081,7 +1043,7 @@ def submission_history(request, course_id, student_username, location): ...@@ -1081,7 +1043,7 @@ def submission_history(request, course_id, student_username, location):
StudentModuleHistory records. StudentModuleHistory records.
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = CourseKey.from_string(course_id)
try: try:
usage_key = course_key.make_usage_key_from_deprecated_string(location) usage_key = course_key.make_usage_key_from_deprecated_string(location)
...@@ -1195,7 +1157,7 @@ def get_course_lti_endpoints(request, course_id): ...@@ -1195,7 +1157,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. (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: try:
course = get_course(course_key, depth=2) course = get_course(course_key, depth=2)
...@@ -1244,7 +1206,7 @@ def course_survey(request, course_id): ...@@ -1244,7 +1206,7 @@ def course_survey(request, course_id):
views.py file in the Survey Djangoapp 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) course = get_course_with_access(request.user, 'load', course_key)
redirect_url = reverse('info', args=[course_id]) redirect_url = reverse('info', args=[course_id])
......
...@@ -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'),
) )
...@@ -4,22 +4,33 @@ Views handling read (GET) requests for the Discussion tab and inline discussions ...@@ -4,22 +4,33 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
from functools import wraps from functools import wraps
import logging import logging
from sets import Set
from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import Http404, HttpResponseBadRequest from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import Http404, HttpResponseServerError
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template.loader import render_to_string
from django.utils.translation import get_language_bidi
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
import newrelic.agent import newrelic.agent
from rest_framework import status
from web_fragments.fragment import Fragment
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from courseware.views.views import CourseTabView
from openedx.core.djangoapps.course_groups.cohorts import ( from openedx.core.djangoapps.course_groups.cohorts import (
is_course_cohorted, is_course_cohorted,
get_cohort_id, get_cohort_id,
get_course_cohorts, get_course_cohorts,
) )
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from courseware.access import has_access from courseware.access import has_access
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -51,8 +62,7 @@ def make_course_settings(course, user): ...@@ -51,8 +62,7 @@ def make_course_settings(course, user):
Generate a JSON-serializable model for course settings, which will be used to initialize a Generate a JSON-serializable model for course settings, which will be used to initialize a
DiscussionCourseSettings object on the client. DiscussionCourseSettings object on the client.
""" """
return {
obj = {
'is_cohorted': is_course_cohorted(course.id), 'is_cohorted': is_course_cohorted(course.id),
'allow_anonymous': course.allow_anonymous, 'allow_anonymous': course.allow_anonymous,
'allow_anonymous_to_peers': course.allow_anonymous_to_peers, 'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
...@@ -60,8 +70,6 @@ def make_course_settings(course, user): ...@@ -60,8 +70,6 @@ def make_course_settings(course, user):
'category_map': utils.get_discussion_category_map(course, user) 'category_map': utils.get_discussion_category_map(course, user)
} }
return obj
@newrelic.agent.function_trace() @newrelic.agent.function_trace()
def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE): def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE):
...@@ -185,7 +193,7 @@ def inline_discussion(request, course_key, discussion_id): ...@@ -185,7 +193,7 @@ def inline_discussion(request, course_key, discussion_id):
try: try:
threads, query_params = get_threads(request, course, user_info, discussion_id, per_page=INLINE_THREADS_PER_PAGE) threads, query_params = get_threads(request, course, user_info, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid group_id") return HttpResponseServerError("Invalid group_id")
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
...@@ -214,8 +222,7 @@ def forum_form_discussion(request, course_key): ...@@ -214,8 +222,7 @@ def forum_form_discussion(request, course_key):
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, request.user) if request.is_ajax():
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user_info = user.to_dict() user_info = user.to_dict()
...@@ -224,13 +231,9 @@ def forum_form_discussion(request, course_key): ...@@ -224,13 +231,9 @@ def forum_form_discussion(request, course_key):
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
except cc.utils.CommentClientMaintenanceError: except cc.utils.CommentClientMaintenanceError:
log.warning("Forum is in maintenance mode") return HttpResponseServerError('Forum is in maintenance mode', status=status.HTTP_503_SERVICE_UNAVAILABLE)
return render_to_response('discussion/maintenance.html', {
'disable_courseware_js': True,
'uses_pattern_library': True,
})
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid group_id") return HttpResponseServerError("Invalid group_id")
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
...@@ -238,7 +241,6 @@ def forum_form_discussion(request, course_key): ...@@ -238,7 +241,6 @@ def forum_form_discussion(request, course_key):
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course, request.user) add_courseware_context(threads, course, request.user)
if request.is_ajax():
return utils.JsonResponse({ return utils.JsonResponse({
'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads'
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
...@@ -247,39 +249,9 @@ def forum_form_discussion(request, course_key): ...@@ -247,39 +249,9 @@ def forum_form_discussion(request, course_key):
'corrected_text': query_params['corrected_text'], 'corrected_text': query_params['corrected_text'],
}) })
else: else:
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): course_id = unicode(course.id)
user_cohort_id = get_cohort_id(request.user, course_key) tab_view = CourseTabView()
return tab_view.get(request, course_id, 'discussion')
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
#'recent_active_threads': recent_active_threads,
'staff_access': bool(has_access(request.user, 'staff', course)),
'threads': threads,
'thread_pages': query_params['num_pages'],
'user_info': user_info,
'can_create_comment': has_permission(request.user, "create_comment", course.id),
'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id),
'flag_moderator': bool(
has_permission(request.user, 'openclose_thread', course.id) or
has_access(request.user, 'staff', course)
),
'annotated_content_info': annotated_content_info,
'course_id': course.id.to_deprecated_string(),
'roles': utils.get_role_ids(course_key),
'is_moderator': has_permission(request.user, "see_all_cohorts", course_key),
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
'user_cohort': user_cohort_id, # read from container in NewPostView
'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template
'sort_preference': user.default_sort_key,
'category_map': course_settings["category_map"],
'course_settings': course_settings,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
# print "start rendering.."
return render_to_response('discussion/discussion_board.html', context)
@require_GET @require_GET
...@@ -296,37 +268,16 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -296,37 +268,16 @@ def single_thread(request, course_key, discussion_id, thread_id):
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, request.user)
if request.is_ajax():
cc_user = cc.User.from_django_user(request.user) cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict() user_info = cc_user.to_dict()
is_moderator = has_permission(request.user, "see_all_cohorts", course_key)
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
try: thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
thread = cc.Thread.find(thread_id).retrieve( if not thread:
with_responses=request.is_ajax(),
recursive=request.is_ajax(),
user_id=request.user.id,
response_skip=request.GET.get("resp_skip"),
response_limit=request.GET.get("resp_limit")
)
except cc.utils.CommentClientRequestError as error:
if error.status_code == 404:
raise Http404
raise
# Verify that the student has access to this thread if belongs to a course discussion module
thread_context = getattr(thread, "context", "course")
if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
raise Http404 raise Http404
# verify that the thread belongs to the requesting student's cohort
if is_commentable_cohorted(course_key, discussion_id) and not is_moderator:
user_group_id = get_cohort_id(request.user, course_key)
if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
raise Http404
if request.is_ajax():
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos( annotated_content_info = utils.get_annotated_content_infos(
course_key, course_key,
...@@ -344,57 +295,136 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -344,57 +295,136 @@ def single_thread(request, course_key, discussion_id, thread_id):
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
else: else:
course_id = unicode(course.id)
tab_view = CourseTabView()
return tab_view.get(request, course_id, 'discussion', discussion_id=discussion_id, thread_id=thread_id)
def _find_thread(request, course, discussion_id, thread_id):
"""
Finds the discussion thread with the specified ID.
Args:
request: The Django request.
course_id: The ID of the owning course.
discussion_id: The ID of the owning discussion.
thread_id: The ID of the thread.
Returns:
The thread in question if the user can see it, else None.
"""
try:
thread = cc.Thread.find(thread_id).retrieve(
with_responses=request.is_ajax(),
recursive=request.is_ajax(),
user_id=request.user.id,
response_skip=request.GET.get("resp_skip"),
response_limit=request.GET.get("resp_limit")
)
except cc.utils.CommentClientRequestError:
return None
# Verify that the student has access to this thread if belongs to a course discussion module
thread_context = getattr(thread, "context", "course")
if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
return None
# verify that the thread belongs to the requesting student's cohort
is_moderator = has_permission(request.user, "see_all_cohorts", course.id)
if is_commentable_cohorted(course.id, discussion_id) and not is_moderator:
user_group_id = get_cohort_id(request.user, course.id)
if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
return None
return thread
def _create_base_discussion_view_context(request, course_key):
"""
Returns the default template context for rendering any discussion view.
"""
user = request.user
cc_user = cc.User.from_django_user(user)
user_info = cc_user.to_dict()
course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, user)
return {
'csrf': csrf(request)['csrf_token'],
'course': course,
'user': user,
'user_info': user_info,
'staff_access': bool(has_access(user, 'staff', course)),
'roles': utils.get_role_ids(course_key),
'can_create_comment': has_permission(user, "create_comment", course.id),
'can_create_subcomment': has_permission(user, "create_sub_comment", course.id),
'can_create_thread': has_permission(user, "create_thread", course.id),
'flag_moderator': bool(
has_permission(user, 'openclose_thread', course.id) or
has_access(user, 'staff', course)
),
'course_settings': course_settings,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
def _create_discussion_board_context(request, course_key, discussion_id=None, thread_id=None):
"""
Returns the template context for rendering the discussion board.
"""
nr_transaction = newrelic.agent.current_transaction()
context = _create_base_discussion_view_context(request, course_key)
course = context['course']
course_settings = context['course_settings']
user = context['user']
cc_user = cc.User.from_django_user(user)
user_info = context['user_info']
if thread_id:
thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
if not thread:
raise Http404
# Since we're in page render mode, and the discussions UI will request the thread list itself, # Since we're in page render mode, and the discussions UI will request the thread list itself,
# we need only return the thread information for this one. # we need only return the thread information for this one.
threads = [thread.to_dict()] threads = [thread.to_dict()]
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course, request.user)
for thread in threads: for thread in threads:
# patch for backward compatibility with comments service # patch for backward compatibility with comments service
if "pinned" not in thread: if "pinned" not in thread:
thread["pinned"] = False thread["pinned"] = False
thread_pages = 1
root_url = reverse('forum_form_discussion', args=[unicode(course.id)])
else:
threads, query_params = get_threads(request, course, user_info) # This might process a search query
thread_pages = query_params['num_pages']
root_url = request.path
is_staff = has_permission(user, 'openclose_thread', course.id)
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_key, threads, user, user_info)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course, user)
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
user_cohort = get_cohort_id(request.user, course_key) user_cohort_id = get_cohort_id(user, course_key)
context = { context.update({
'root_url': root_url,
'discussion_id': discussion_id, 'discussion_id': discussion_id,
'csrf': csrf(request)['csrf_token'],
'init': '', # TODO: What is this?
'user_info': user_info,
'can_create_comment': has_permission(request.user, "create_comment", course.id),
'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id),
'annotated_content_info': annotated_content_info,
'course': course,
#'recent_active_threads': recent_active_threads,
'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template?
'thread_id': thread_id, 'thread_id': thread_id,
'threads': threads, 'threads': threads,
'roles': utils.get_role_ids(course_key), 'thread_pages': thread_pages,
'is_moderator': is_moderator, 'annotated_content_info': annotated_content_info,
'thread_pages': 1, 'is_moderator': has_permission(user, "see_all_cohorts", course_key),
'is_course_cohorted': is_course_cohorted(course_key), 'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
'flag_moderator': bool( 'user_cohort': user_cohort_id, # read from container in NewPostView
has_permission(request.user, 'openclose_thread', course.id) or
has_access(request.user, 'staff', course)
),
'cohorts': course_settings["cohorts"],
'user_cohort': user_cohort,
'sort_preference': cc_user.default_sort_key, 'sort_preference': cc_user.default_sort_key,
'category_map': course_settings["category_map"], 'category_map': course_settings["category_map"],
'course_settings': course_settings, 'course_settings': course_settings,
'disable_courseware_js': True, })
'uses_pattern_library': True, return context
}
return render_to_response('discussion/discussion_board.html', context)
@require_GET @require_GET
...@@ -409,9 +439,7 @@ def user_profile(request, course_key, user_id): ...@@ -409,9 +439,7 @@ def user_profile(request, course_key, user_id):
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user_info = user.to_dict()
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_settings = make_course_settings(course, request.user)
try: try:
# If user is not enrolled in the course, do not proceed. # If user is not enrolled in the course, do not proceed.
...@@ -427,7 +455,7 @@ def user_profile(request, course_key, user_id): ...@@ -427,7 +455,7 @@ def user_profile(request, course_key, user_id):
try: try:
group_id = get_group_id_for_comments_service(request, course_key) group_id = get_group_id_for_comments_service(request, course_key)
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid group_id") return HttpResponseServerError("Invalid group_id")
if group_id is not None: if group_id is not None:
query_params['group_id'] = group_id query_params['group_id'] = group_id
profiled_user = cc.User(id=user_id, course_id=course_key, group_id=group_id) profiled_user = cc.User(id=user_id, course_id=course_key, group_id=group_id)
...@@ -437,9 +465,9 @@ def user_profile(request, course_key, user_id): ...@@ -437,9 +465,9 @@ def user_profile(request, course_key, user_id):
threads, page, num_pages = profiled_user.active_threads(query_params) threads, page, num_pages = profiled_user.active_threads(query_params)
query_params['page'] = page query_params['page'] = page
query_params['num_pages'] = num_pages query_params['num_pages'] = num_pages
user_info = cc.User.from_django_user(request.user).to_dict()
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
is_staff = has_permission(request.user, 'openclose_thread', course.id) is_staff = has_permission(request.user, 'openclose_thread', course.id)
...@@ -461,32 +489,19 @@ def user_profile(request, course_key, user_id): ...@@ -461,32 +489,19 @@ def user_profile(request, course_key, user_id):
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
user_cohort_id = get_cohort_id(request.user, course_key) user_cohort_id = get_cohort_id(request.user, course_key)
context = { context = _create_base_discussion_view_context(request, course_key)
'course': course, context.update({
'user': request.user,
'django_user': django_user, 'django_user': django_user,
'django_user_roles': user_roles, 'django_user_roles': user_roles,
'profiled_user': profiled_user.to_dict(), 'profiled_user': profiled_user.to_dict(),
'threads': threads, 'threads': threads,
'user_info': user_info,
'roles': utils.get_role_ids(course_key),
'can_create_comment': has_permission(request.user, "create_comment", course.id),
'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id),
'flag_moderator': bool(
has_permission(request.user, 'openclose_thread', course.id) or
has_access(request.user, 'staff', course)
),
'user_cohort': user_cohort_id, 'user_cohort': user_cohort_id,
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
'sort_preference': user.default_sort_key, 'sort_preference': user.default_sort_key,
'course_settings': course_settings,
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}), 'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}),
'disable_courseware_js': True, })
'uses_pattern_library': True,
}
return render_to_response('discussion/discussion_profile_page.html', context) return render_to_response('discussion/discussion_profile_page.html', context)
except User.DoesNotExist: except User.DoesNotExist:
...@@ -531,7 +546,7 @@ def followed_threads(request, course_key, user_id): ...@@ -531,7 +546,7 @@ def followed_threads(request, course_key, user_id):
try: try:
group_id = get_group_id_for_comments_service(request, course_key) group_id = get_group_id_for_comments_service(request, course_key)
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid group_id") return HttpResponseServerError("Invalid group_id")
if group_id is not None: if group_id is not None:
query_params['group_id'] = group_id query_params['group_id'] = group_id
...@@ -574,3 +589,81 @@ def followed_threads(request, course_key, user_id): ...@@ -574,3 +589,81 @@ def followed_threads(request, course_key, user_id):
return render_to_response('discussion/user_profile.html', context) return render_to_response('discussion/user_profile.html', context)
except User.DoesNotExist: except User.DoesNotExist:
raise Http404 raise Http404
class DiscussionBoardFragmentView(EdxFragmentView):
"""
Component implementation of the discussion board.
"""
def render_to_fragment(self, request, course_id=None, discussion_id=None, thread_id=None, **kwargs):
"""
Render the discussion board to a fragment.
Args:
request: The Django request.
course_id: The id of the course in question.
discussion_id: An optional discussion ID to be focused upon.
thread_id: An optional ID of the thread to be shown.
Returns:
Fragment: The fragment representing the discussion board
"""
course_key = CourseKey.from_string(course_id)
try:
context = _create_discussion_board_context(
request,
course_key,
discussion_id=discussion_id,
thread_id=thread_id,
)
html = render_to_string('discussion/discussion_board_fragment.html', context)
inline_js = render_to_string('discussion/discussion_board_js.template', context)
fragment = Fragment(html)
self.add_fragment_resource_urls(fragment)
fragment.add_javascript(inline_js)
if not settings.REQUIRE_DEBUG:
fragment.add_javascript_url(staticfiles_storage.url('discussion/js/discussion_board_factory.js'))
return fragment
except cc.utils.CommentClientMaintenanceError:
log.warning('Forum is in maintenance mode')
html = render_to_response('discussion/maintenance_fragment.html', {
'disable_courseware_js': True,
'uses_pattern_library': True,
})
return Fragment(html)
def vendor_js_dependencies(self):
"""
Returns list of vendor JS files that this view depends on.
The helper function that it uses to obtain the list of vendor JS files
works in conjunction with the Django pipeline to ensure that in development mode
the files are loaded individually, but in production just the single bundle is loaded.
"""
dependencies = Set()
dependencies.update(self.get_js_dependencies('discussion_vendor'))
return list(dependencies)
def js_dependencies(self):
"""
Returns list of JS files that this view depends on.
The helper function that it uses to obtain the list of JS files
works in conjunction with the Django pipeline to ensure that in development mode
the files are loaded individually, but in production just the single bundle is loaded.
"""
return self.get_js_dependencies('discussion')
def css_dependencies(self):
"""
Returns list of CSS files that this view depends on.
The helper function that it uses to obtain the list of CSS files
works in conjunction with the Django pipeline to ensure that in development mode
the files are loaded individually, but in production just the single bundle is loaded.
"""
if get_language_bidi():
return self.get_css_dependencies('style-discussion-main-rtl')
else:
return self.get_css_dependencies('style-discussion-main')
...@@ -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
try:
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
self.assertEqual(response.status_code, 400) 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