Render Discussion tab using web fragments
This commit is contained in:
@@ -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 () {
|
||||
|
||||
@@ -15,6 +15,9 @@ log = logging.getLogger("edx.courseware")
|
||||
# `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):
|
||||
"""
|
||||
@@ -33,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
|
||||
|
||||
@@ -70,7 +79,7 @@ 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.course_staff_only = tab_dict.get('course_staff_only', False)
|
||||
@@ -80,6 +89,13 @@ class CourseTab(object):
|
||||
|
||||
@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
|
||||
@@ -107,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()))
|
||||
|
||||
@@ -127,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()))
|
||||
|
||||
@@ -236,28 +238,52 @@ class CourseTab(object):
|
||||
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
|
||||
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 url for a given course and reverse function. """
|
||||
return reverse_func("content_tab", args=[course.id.to_deprecated_string(), self.type])
|
||||
""" 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):
|
||||
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):
|
||||
component = get_storage_class(self.class_name)()
|
||||
fragment = component.render_component(request, course_id=course.id.to_deprecated_string())
|
||||
return fragment
|
||||
@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):
|
||||
@@ -270,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,7 @@ 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 xmodule.tabs import ComponentTabMixin, CourseTab, CourseTabList, key_checker
|
||||
from xmodule.tabs import CourseTab, CourseTabList, key_checker
|
||||
|
||||
|
||||
class EnrolledTab(CourseTab):
|
||||
@@ -70,14 +70,14 @@ class SyllabusTab(EnrolledTab):
|
||||
return getattr(course, 'syllabus_present', False)
|
||||
|
||||
|
||||
class ProgressTab(ComponentTabMixin, EnrolledTab):
|
||||
class ProgressTab(EnrolledTab):
|
||||
"""
|
||||
The course progress view.
|
||||
"""
|
||||
type = 'progress'
|
||||
title = ugettext_noop('Progress')
|
||||
priority = 40
|
||||
class_name="courseware.views.views.ProgressComponentView"
|
||||
view_name = 'progress'
|
||||
is_hideable = True
|
||||
is_default = False
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -18,7 +18,13 @@ 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
|
||||
@@ -32,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
|
||||
@@ -99,8 +104,8 @@ 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.views import FragmentView
|
||||
from web_fragments.fragment import Fragment
|
||||
from web_fragments.views import FragmentView
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
@@ -443,63 +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)
|
||||
|
||||
tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
|
||||
if tab is None:
|
||||
raise Http404
|
||||
|
||||
fragment = get_static_tab_fragment(
|
||||
request,
|
||||
course,
|
||||
tab
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@ensure_valid_course_key
|
||||
def content_tab(request, course_id, tab_type):
|
||||
class CourseTabView(FragmentView):
|
||||
"""
|
||||
Display a content tab based on type name.
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
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)
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
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)
|
||||
|
||||
content_tab = [tab for tab in course.tabs if tab.type == tab_type][0]
|
||||
fragment = content_tab.render_fragment(request, course)
|
||||
|
||||
|
||||
return render_to_response('courseware/static_tab.html', {
|
||||
'course': course,
|
||||
'active_page': content_tab['type'],
|
||||
'tab': content_tab,
|
||||
'fragment': fragment,
|
||||
'uses_pattern_library': True,
|
||||
'disable_courseware_js': True
|
||||
})
|
||||
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
|
||||
@@ -511,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))
|
||||
@@ -619,7 +640,7 @@ def course_about(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)
|
||||
|
||||
if hasattr(course_key, 'ccx'):
|
||||
# if un-enrolled/non-registered user try to access CCX (direct for registration)
|
||||
@@ -749,65 +770,6 @@ def course_about(request, course_id):
|
||||
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
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@@ -1081,7 +1043,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)
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
try:
|
||||
course = get_course(course_key, depth=2)
|
||||
@@ -1244,7 +1206,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,10 +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 ComponentTabMixin
|
||||
from xmodule.tabs import TabFragmentViewMixin
|
||||
|
||||
|
||||
class DiscussionTab(ComponentTabMixin, EnrolledTab):
|
||||
class DiscussionTab(TabFragmentViewMixin, EnrolledTab):
|
||||
"""
|
||||
A tab for the cs_comments_service forums.
|
||||
"""
|
||||
@@ -18,9 +18,12 @@ class DiscussionTab(ComponentTabMixin, EnrolledTab):
|
||||
type = 'discussion'
|
||||
title = ugettext_noop('Discussion')
|
||||
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_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);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
## 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" />
|
||||
@@ -0,0 +1,63 @@
|
||||
## 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);
|
||||
@@ -1,16 +1,10 @@
|
||||
## 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'),
|
||||
)
|
||||
|
||||
@@ -4,22 +4,33 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
|
||||
|
||||
from functools import wraps
|
||||
import logging
|
||||
from sets import Set
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
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.template.loader import render_to_string
|
||||
from django.utils.translation import get_language_bidi
|
||||
from django.views.decorators.http import require_GET
|
||||
import newrelic.agent
|
||||
from rest_framework import status
|
||||
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.views.views import CourseTabView
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
is_course_cohorted,
|
||||
get_cohort_id,
|
||||
get_course_cohorts,
|
||||
)
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
|
||||
from courseware.access import has_access
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -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
|
||||
DiscussionCourseSettings object on the client.
|
||||
"""
|
||||
|
||||
obj = {
|
||||
return {
|
||||
'is_cohorted': is_course_cohorted(course.id),
|
||||
'allow_anonymous': course.allow_anonymous,
|
||||
'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
|
||||
@@ -60,8 +70,6 @@ def make_course_settings(course, user):
|
||||
'category_map': utils.get_discussion_category_map(course, user)
|
||||
}
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
@newrelic.agent.function_trace()
|
||||
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):
|
||||
try:
|
||||
threads, query_params = get_threads(request, course, user_info, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("Invalid group_id")
|
||||
return HttpResponseServerError("Invalid group_id")
|
||||
|
||||
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)
|
||||
@@ -214,31 +222,25 @@ def forum_form_discussion(request, course_key):
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_settings = make_course_settings(course, request.user)
|
||||
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user_info = user.to_dict()
|
||||
|
||||
try:
|
||||
unsafethreads, query_params = get_threads(request, course, user_info) # This might process a search query
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
|
||||
except cc.utils.CommentClientMaintenanceError:
|
||||
log.warning("Forum is in maintenance mode")
|
||||
return render_to_response('discussion/maintenance.html', {
|
||||
'disable_courseware_js': True,
|
||||
'uses_pattern_library': True,
|
||||
})
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("Invalid group_id")
|
||||
|
||||
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)
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course, request.user)
|
||||
|
||||
if request.is_ajax():
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user_info = user.to_dict()
|
||||
|
||||
try:
|
||||
unsafethreads, query_params = get_threads(request, course, user_info) # This might process a search query
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
|
||||
except cc.utils.CommentClientMaintenanceError:
|
||||
return HttpResponseServerError('Forum is in maintenance mode', status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
except ValueError:
|
||||
return HttpResponseServerError("Invalid group_id")
|
||||
|
||||
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)
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course, request.user)
|
||||
|
||||
return utils.JsonResponse({
|
||||
'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads'
|
||||
'annotated_content_info': annotated_content_info,
|
||||
@@ -247,39 +249,9 @@ def forum_form_discussion(request, course_key):
|
||||
'corrected_text': query_params['corrected_text'],
|
||||
})
|
||||
else:
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
|
||||
user_cohort_id = get_cohort_id(request.user, course_key)
|
||||
|
||||
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)
|
||||
course_id = unicode(course.id)
|
||||
tab_view = CourseTabView()
|
||||
return tab_view.get(request, course_id, 'discussion')
|
||||
|
||||
|
||||
@require_GET
|
||||
@@ -296,37 +268,16 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_settings = make_course_settings(course, request.user)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
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)
|
||||
|
||||
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 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
|
||||
|
||||
# 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():
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
user_info = cc_user.to_dict()
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
|
||||
thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
|
||||
if not thread:
|
||||
raise Http404
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
|
||||
annotated_content_info = utils.get_annotated_content_infos(
|
||||
course_key,
|
||||
@@ -344,57 +295,136 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
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,
|
||||
# we need only return the thread information for this one.
|
||||
threads = [thread.to_dict()]
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course, request.user)
|
||||
|
||||
for thread in threads:
|
||||
# patch for backward compatibility with comments service
|
||||
if "pinned" not in thread:
|
||||
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"):
|
||||
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, user, user_info)
|
||||
|
||||
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)
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course, user)
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
|
||||
user_cohort = get_cohort_id(request.user, course_key)
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
|
||||
user_cohort_id = get_cohort_id(user, course_key)
|
||||
|
||||
context = {
|
||||
'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,
|
||||
'threads': threads,
|
||||
'roles': utils.get_role_ids(course_key),
|
||||
'is_moderator': is_moderator,
|
||||
'thread_pages': 1,
|
||||
'is_course_cohorted': is_course_cohorted(course_key),
|
||||
'flag_moderator': bool(
|
||||
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,
|
||||
'category_map': course_settings["category_map"],
|
||||
'course_settings': course_settings,
|
||||
'disable_courseware_js': True,
|
||||
'uses_pattern_library': True,
|
||||
}
|
||||
return render_to_response('discussion/discussion_board.html', context)
|
||||
context.update({
|
||||
'root_url': root_url,
|
||||
'discussion_id': discussion_id,
|
||||
'thread_id': thread_id,
|
||||
'threads': threads,
|
||||
'thread_pages': thread_pages,
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'is_moderator': has_permission(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
|
||||
'sort_preference': cc_user.default_sort_key,
|
||||
'category_map': course_settings["category_map"],
|
||||
'course_settings': course_settings,
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
@require_GET
|
||||
@@ -409,9 +439,7 @@ def user_profile(request, course_key, user_id):
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
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_settings = make_course_settings(course, request.user)
|
||||
|
||||
try:
|
||||
# If user is not enrolled in the course, do not proceed.
|
||||
@@ -427,7 +455,7 @@ def user_profile(request, course_key, user_id):
|
||||
try:
|
||||
group_id = get_group_id_for_comments_service(request, course_key)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("Invalid group_id")
|
||||
return HttpResponseServerError("Invalid group_id")
|
||||
if group_id is not None:
|
||||
query_params['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):
|
||||
threads, page, num_pages = profiled_user.active_threads(query_params)
|
||||
query_params['page'] = page
|
||||
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"):
|
||||
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)
|
||||
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
@@ -461,32 +489,19 @@ def user_profile(request, course_key, user_id):
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
|
||||
user_cohort_id = get_cohort_id(request.user, course_key)
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'user': request.user,
|
||||
context = _create_base_discussion_view_context(request, course_key)
|
||||
context.update({
|
||||
'django_user': django_user,
|
||||
'django_user_roles': user_roles,
|
||||
'profiled_user': profiled_user.to_dict(),
|
||||
'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,
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'sort_preference': user.default_sort_key,
|
||||
'course_settings': course_settings,
|
||||
'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)
|
||||
except User.DoesNotExist:
|
||||
@@ -531,7 +546,7 @@ def followed_threads(request, course_key, user_id):
|
||||
try:
|
||||
group_id = get_group_id_for_comments_service(request, course_key)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("Invalid group_id")
|
||||
return HttpResponseServerError("Invalid group_id")
|
||||
if group_id is not None:
|
||||
query_params['group_id'] = group_id
|
||||
|
||||
@@ -574,3 +589,81 @@ def followed_threads(request, course_key, user_id):
|
||||
return render_to_response('discussion/user_profile.html', context)
|
||||
except User.DoesNotExist:
|
||||
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
|
||||
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
|
||||
@@ -1927,6 +1930,10 @@ INSTALLED_APPS = (
|
||||
'pipeline',
|
||||
'static_replace',
|
||||
|
||||
# For user interface plugins
|
||||
'web_fragments',
|
||||
'openedx.core.djangoapps.plugin_api',
|
||||
|
||||
# For content serving
|
||||
'openedx.core.djangoapps.contentserver',
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## 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)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
## mako
|
||||
|
||||
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
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>
|
||||
<%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 | h}</%block>
|
||||
<%block name="pagetitle">${tab['name']} | ${course.display_number_with_default}</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page=active_page" />
|
||||
|
||||
|
||||
32
lms/templates/courseware/tab-view.html
Normal file
32
lms/templates/courseware/tab-view.html
Normal file
@@ -0,0 +1,32 @@
|
||||
## 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>
|
||||
24
lms/templates/fragment-view-chromeless.html
Normal file
24
lms/templates/fragment-view-chromeless.html
Normal file
@@ -0,0 +1,24 @@
|
||||
## 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
|
||||
@@ -691,8 +691,8 @@ urlpatterns += (
|
||||
r'^courses/{}/tab/(?P<tab_type>[^/]+)/$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'courseware.views.views.content_tab',
|
||||
name='content_tab',
|
||||
CourseTabView.as_view(),
|
||||
name='course_tab_view',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -702,7 +702,7 @@ urlpatterns += (
|
||||
r'^courses/{}/(?P<tab_slug>[^/]+)/$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'courseware.views.views.static_tab',
|
||||
StaticCourseTabView.as_view(),
|
||||
name='static_tab',
|
||||
),
|
||||
)
|
||||
|
||||
0
openedx/core/djangoapps/plugin_api/__init__.py
Normal file
0
openedx/core/djangoapps/plugin_api/__init__.py
Normal file
99
openedx/core/djangoapps/plugin_api/views.py
Normal file
99
openedx/core/djangoapps/plugin_api/views.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user