Commit 33b3dfcc by E. Kolpakov Committed by Jillian Vogel

Converts Discussion XModule to Discussion XBlock

* Renames discussion_module to discussion_xblock
* Moves common/lib/xmodule/xmodule_discussion to openedx/core/lib/xblock_builtin/xblock_discussion
parent e7527fca
......@@ -27,6 +27,8 @@ def create_component_instance(step, category, component_type=None, is_advanced=F
module_css = 'div.xmodule_CapaModule'
elif category == 'advanced':
module_css = 'div.xmodule_{}Module'.format(advanced_component.title())
elif category == 'discussion':
module_css = 'div.xblock-author_view-{}'.format(category.lower())
else:
module_css = 'div.xmodule_{}Module'.format(category.title())
......@@ -168,7 +170,9 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
for the problem, rather than derived from the defaults. This is verified
by the existence of a "Clear" button next to the field value.
"""
assert_equal(display_name, setting.find_by_css('.setting-label')[0].html.strip())
label_element = setting.find_by_css('.setting-label')[0]
assert_equal(display_name, label_element.html.strip())
label_for = label_element['for']
# Check if the web object is a list type
# If so, we use a slightly different mechanism for determining its value
......@@ -179,7 +183,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
list_value = ', '.join(ele.find_by_css('input')[0].value for ele in setting.find_by_css('.videolist-settings-item'))
assert_equal(value, list_value)
else:
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
assert_equal(value, setting.find_by_id(label_for).value)
# VideoList doesn't have clear button
if not setting.has_class('metadata-videolist-enum'):
......@@ -201,7 +205,7 @@ def verify_all_setting_entries(expected_entries):
@world.absorb
def save_component():
world.css_click("a.action-save")
world.css_click("a.action-save,a.save-button")
world.wait_for_ajax_complete()
......@@ -241,7 +245,7 @@ def get_setting_entry(label):
@world.absorb
def get_setting_entry_index(label):
def get_index():
settings = world.css_find('.metadata_edit .wrapper-comp-setting')
settings = world.css_find('.wrapper-comp-setting')
for index, setting in enumerate(settings):
if setting.find_by_css('.setting-label')[0].value == label:
return index
......@@ -259,6 +263,6 @@ def set_field_value(index, value):
Instead we will find the element, set its value, then hit the Tab key
to get to the next field.
"""
elem = world.css_find('.metadata_edit div.wrapper-comp-setting input.setting-input')[index]
elem = world.css_find('div.wrapper-comp-setting input')[index]
elem.value = value
elem.type(Keys.TAB)
......@@ -5,7 +5,7 @@ Feature: CMS.Discussion Component Editor
Scenario: User can view discussion component metadata
Given I have created a Discussion Tag
And I edit the component
Then I see three alphabetized settings and their expected values
Then I see three settings and their expected values
# Safari doesn't save the name properly
@skip_safari
......
......@@ -13,12 +13,12 @@ def i_created_discussion_tag(step):
)
@step('I see three alphabetized settings and their expected values$')
@step('I see three settings and their expected values$')
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", False],
['Display Name', "Discussion", False],
['Category', "Week 1", False],
['Subcategory', "Topic-Level Student-Visible Label", False]
])
......
......@@ -23,7 +23,6 @@ XMODULES = [
"videoalpha = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor",
"course_info = xmodule.html_module:CourseInfoDescriptor",
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
......
"""
Definition of the Discussion module.
"""
import json
from pkg_resources import resource_string
from xblock.core import XBlock
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.fields import String, Scope, UNIQUE_ID
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text
class DiscussionFields(object):
discussion_id = String(
display_name=_("Discussion Id"),
help=_("The id is a unique identifier for the discussion. It is non editable."),
scope=Scope.settings,
default=UNIQUE_ID)
display_name = String(
display_name=_("Display Name"),
help=_("Display name for this module"),
default="Discussion",
scope=Scope.settings
)
data = String(
help=_("XML data for the problem"),
scope=Scope.content,
default="<discussion></discussion>"
)
discussion_category = String(
display_name=_("Category"),
default="Week 1",
help=_("A category name for the discussion. This name appears in the left pane of the discussion forum for the course."),
scope=Scope.settings
)
discussion_target = String(
display_name=_("Subcategory"),
default="Topic-Level Student-Visible Label",
help=_("A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course."),
scope=Scope.settings
)
sort_key = String(scope=Scope.settings)
def has_permission(user, permission, course_id):
"""
Copied from django_comment_client/permissions.py because I can't import
that file from here. It causes the xmodule_assets command to fail.
"""
return any(role.has_permission(permission)
for role in user.roles.filter(course_id=course_id))
@XBlock.wants('user')
class DiscussionModule(DiscussionFields, XModule):
"""
XModule for discussion forums.
"""
js = {
'coffee': [
resource_string(__name__, 'js/src/discussion/display.coffee')
],
'js': [
resource_string(__name__, 'js/src/time.js')
]
}
js_module_name = "InlineDiscussion"
def get_html(self):
course = self.get_course()
user = None
user_service = self.runtime.service(self, 'user')
if user_service:
user = user_service._django_user # pylint: disable=protected-access
if user:
course_key = course.id
can_create_comment = has_permission(user, "create_comment", course_key)
can_create_subcomment = has_permission(user, "create_sub_comment", course_key)
can_create_thread = has_permission(user, "create_thread", course_key)
else:
can_create_comment = False
can_create_subcomment = False
can_create_thread = False
context = {
'discussion_id': self.discussion_id,
'course': course,
'can_create_comment': json.dumps(can_create_comment),
'can_create_subcomment': json.dumps(can_create_subcomment),
'can_create_thread': can_create_thread,
}
if getattr(self.system, 'is_author_mode', False):
template = 'discussion/_discussion_module_studio.html'
else:
template = 'discussion/_discussion_module.html'
return self.system.render_template(template, context)
def get_course(self):
"""
Return CourseDescriptor by course id.
"""
course = self.runtime.modulestore.get_course(self.course_id)
return course
class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor):
module_class = DiscussionModule
resources_dir = None
# The discussion XML format uses `id` and `for` attributes,
# but these would overload other module attributes, so we prefix them
# for actual use in the code
metadata_translations = dict(RawDescriptor.metadata_translations)
metadata_translations['id'] = 'discussion_id'
metadata_translations['for'] = 'discussion_target'
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(DiscussionDescriptor, self).non_editable_metadata_fields
# We may choose to enable sort_keys in the future, but while Kevin is investigating....
non_editable_fields.extend([DiscussionDescriptor.discussion_id, DiscussionDescriptor.sort_key])
return non_editable_fields
def student_view_data(self):
"""
Returns a JSON representation of the student_view of this XModule.
"""
return {'topic_id': self.discussion_id}
......@@ -31,7 +31,6 @@ from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, Descripto
from xmodule.annotatable_module import AnnotatableDescriptor
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.discussion_module import DiscussionDescriptor
from xmodule.html_module import HtmlDescriptor
from xmodule.poll_module import PollDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
......@@ -50,7 +49,6 @@ from xmodule.tests import get_test_descriptor_system, get_test_system
LEAF_XMODULES = {
AnnotatableDescriptor: [{}],
CapaDescriptor: [{}],
DiscussionDescriptor: [{}],
HtmlDescriptor: [{}],
PollDescriptor: [{'display_name': 'Poll Display Name'}],
WordCloudDescriptor: [{}],
......
......@@ -112,6 +112,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
#TODO: For each of the following, ensure that any generated html is properly escaped.
js = {
'js': [
resource_string(module, 'js/src/time.js'),
resource_string(module, 'js/src/video/00_component.js'),
resource_string(module, 'js/src/video/00_video_storage.js'),
resource_string(module, 'js/src/video/00_resizer.js'),
......@@ -350,7 +351,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
'cdn_eval': cdn_eval,
'cdn_exp_group': cdn_exp_group,
'id': self.location.html_id(),
'display_name': self.display_name_with_default_escaped,
'display_name': self.display_name_with_default,
'handout': self.handout,
'download_video_link': download_video_link,
'track': track_url,
......
......@@ -966,22 +966,22 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix
self.assertFalse(self.thread_page.is_comment_deletable("comment1"))
self.assertFalse(self.thread_page.is_comment_deletable("comment2"))
def test_dual_discussion_module(self):
def test_dual_discussion_xblock(self):
"""
Scenario: Two discussion module in one unit shouldn't override their actions
Scenario: Two discussion xblocks in one unit shouldn't override their actions
Given that I'm on courseware page where there are two inline discussion
When I click on one discussion module new post button
Then it should add new post form of that module in DOM
And I should be shown new post form of that module
And I shouldn't be shown second discussion module new post form
And I click on second discussion module new post button
Then it should add new post form of second module in DOM
When I click on one discussion xblock new post button
Then it should add new post form of that xblock in DOM
And I should be shown new post form of that xblock
And I shouldn't be shown second discussion xblock new post form
And I click on second discussion xblock new post button
Then it should add new post form of second xblock in DOM
And I should be shown second discussion new post form
And I shouldn't be shown first discussion module new post form
And I shouldn't be shown first discussion xblock new post form
And I have two new post form in the DOM
When I click back on first module new post button
And I should be shown new post form of that module
And I shouldn't be shown second discussion module new post form
When I click back on first xblock new post button
And I should be shown new post form of that xblock
And I shouldn't be shown second discussion xblock new post form
"""
self.discussion_page.wait_for_page()
self.additional_discussion_page.wait_for_page()
......
......@@ -22,9 +22,10 @@ from textwrap import dedent
from django.core.management.base import BaseCommand, CommandError
from xmodule.discussion_module import DiscussionDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata, compute_inherited_metadata
from xblock_discussion import DiscussionXBlock
from xblock.fields import Scope
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -97,7 +98,7 @@ def dump_module(module, destination=None, inherited=False, defaults=False):
items = own_metadata(module)
# HACK: add discussion ids to list of items to export (AN-6696)
if isinstance(module, DiscussionDescriptor) and 'discussion_id' not in items:
if isinstance(module, DiscussionXBlock) and 'discussion_id' not in items:
items['discussion_id'] = module.discussion_id
filtered_metadata = {k: v for k, v in items.iteritems() if k not in FILTER_LIST}
......
# -*- coding: utf-8 -*-
"""Test for Discussion Xmodule functional logic."""
import ddt
from django.core.urlresolvers import reverse
from mock import Mock
from . import BaseTestXmodule
from course_api.blocks.tests.helpers import deserialize_usage_key
from courseware.module_render import get_module_for_descriptor_internal
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.discussion_module import DiscussionModule
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import ToyCourseFactory, ItemFactory
@ddt.ddt
class DiscussionModuleTest(BaseTestXmodule, SharedModuleStoreTestCase):
"""Logic tests for Discussion Xmodule."""
CATEGORY = "discussion"
def test_html_with_user(self):
discussion = get_module_for_descriptor_internal(
user=self.users[0],
descriptor=self.item_descriptor,
student_data=Mock(name='student_data'),
course_id=self.course.id,
track_function=Mock(name='track_function'),
xqueue_callback_url_prefix=Mock(name='xqueue_callback_url_prefix'),
request_token='request_token',
)
fragment = discussion.render('student_view')
html = fragment.content
self.assertIn('data-user-create-comment="false"', html)
self.assertIn('data-user-create-subcomment="false"', html)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_discussion_render_successfully_with_orphan_parent(self, default_store):
"""
Test that discussion module render successfully
if discussion module is child of an orphan.
"""
user = UserFactory.create()
store = modulestore()
with store.default_store(default_store):
course = store.create_course('testX', 'orphan', '123X', user.id)
orphan_sequential = store.create_item(self.user.id, course.id, 'sequential')
vertical = store.create_child(
user.id,
orphan_sequential.location,
'vertical',
block_id=course.location.block_id
)
discussion = store.create_child(
user.id,
vertical.location,
'discussion',
block_id=course.location.block_id
)
discussion = store.get_item(discussion.location)
root = self.get_root(discussion)
# Assert that orphan sequential is root of the discussion module.
self.assertEqual(orphan_sequential.location.block_type, root.location.block_type)
self.assertEqual(orphan_sequential.location.block_id, root.location.block_id)
# Get module system bound to a user and a descriptor.
discussion_module = get_module_for_descriptor_internal(
user=user,
descriptor=discussion,
student_data=Mock(name='student_data'),
course_id=course.id,
track_function=Mock(name='track_function'),
xqueue_callback_url_prefix=Mock(name='xqueue_callback_url_prefix'),
request_token='request_token',
)
fragment = discussion_module.render('student_view')
html = fragment.content
self.assertIsInstance(discussion_module._xmodule, DiscussionModule) # pylint: disable=protected-access
self.assertIn('data-user-create-comment="false"', html)
self.assertIn('data-user-create-subcomment="false"', html)
def get_root(self, block):
"""
Return root of the block.
"""
while block.parent:
block = block.get_parent()
return block
def test_discussion_student_view_data(self):
"""
Tests that course block api returns student_view_data for discussion module
"""
course_key = ToyCourseFactory.create().id
course_usage_key = self.store.make_course_usage_key(course_key)
user = UserFactory.create()
self.client.login(username=user.username, password='test')
CourseEnrollmentFactory.create(user=user, course_id=course_key)
discussion_id = "test_discussion_module_id"
ItemFactory.create(
parent_location=course_usage_key,
category='discussion',
discussion_id=discussion_id,
discussion_category='Category discussion',
discussion_target='Target Discussion',
)
url = reverse('blocks_in_block_tree', kwargs={'usage_key_string': unicode(course_usage_key)})
query_params = {
'depth': 'all',
'username': user.username,
'block_types_filter': 'discussion',
'student_view_data': 'discussion'
}
response = self.client.get(url, query_params)
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data['root'], unicode(course_usage_key)) # pylint: disable=no-member
for block_key_string, block_data in response.data['blocks'].iteritems(): # pylint: disable=no-member
block_key = deserialize_usage_key(block_key_string, course_key)
self.assertEquals(block_data['id'], block_key_string)
self.assertEquals(block_data['type'], block_key.block_type)
self.assertEquals(block_data['display_name'], self.store.get_item(block_key).display_name or '')
self.assertEqual(block_data['student_view_data'], {"topic_id": discussion_id})
......@@ -9,7 +9,7 @@ from mock import patch
from courseware.tests.factories import BetaTesterFactory
from courseware.access import has_access
from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides
from lms.djangoapps.django_comment_client.utils import get_accessible_discussion_modules
from lms.djangoapps.django_comment_client.utils import get_accessible_discussion_xblocks
from lms.djangoapps.courseware.field_overrides import OverrideFieldData, OverrideModulestoreFieldData
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -59,8 +59,8 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
inject_field_overrides((course, section), course, self.user)
return (course, section)
def create_discussion_modules(self, parent):
# Create a released discussion module
def create_discussion_xblocks(self, parent):
# Create a released discussion xblock
ItemFactory.create(
parent=parent,
category='discussion',
......@@ -68,7 +68,7 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
start=self.now,
)
# Create a scheduled discussion module
# Create a scheduled discussion xblock
ItemFactory.create(
parent=parent,
category='discussion',
......@@ -118,32 +118,32 @@ class SelfPacedDateOverrideTest(ModuleStoreTestCase):
self.assertTrue(has_access(beta_tester, 'load', self_paced_section, self_paced_course.id))
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_instructor_paced_discussion_module_visibility(self):
def test_instructor_paced_discussion_xblock_visibility(self):
"""
Verify that discussion modules scheduled for release in the future are
Verify that discussion xblocks scheduled for release in the future are
not visible to students in an instructor-paced course.
"""
course, section = self.setup_course(start=self.now, self_paced=False)
self.create_discussion_modules(section)
self.create_discussion_xblocks(section)
# Only the released module should be visible when the course is instructor-paced.
modules = get_accessible_discussion_modules(course, self.non_staff_user)
# Only the released xblocks should be visible when the course is instructor-paced.
xblocks = get_accessible_discussion_xblocks(course, self.non_staff_user)
self.assertTrue(
all(module.display_name == 'released' for module in modules)
all(xblock.display_name == 'released' for xblock in xblocks)
)
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_self_paced_discussion_module_visibility(self):
def test_self_paced_discussion_xblock_visibility(self):
"""
Regression test. Verify that discussion modules scheduled for release
Regression test. Verify that discussion xblocks scheduled for release
in the future are visible to students in a self-paced course.
"""
course, section = self.setup_course(start=self.now, self_paced=True)
self.create_discussion_modules(section)
self.create_discussion_xblocks(section)
# The scheduled module should be visible when the course is self-paced.
modules = get_accessible_discussion_modules(course, self.non_staff_user)
self.assertEqual(len(modules), 2)
# The scheduled xblocks should be visible when the course is self-paced.
xblocks = get_accessible_discussion_xblocks(course, self.non_staff_user)
self.assertEqual(len(xblocks), 2)
self.assertTrue(
any(module.display_name == 'scheduled' for module in modules)
any(xblock.display_name == 'scheduled' for xblock in xblocks)
)
......@@ -42,7 +42,7 @@ from django_comment_common.signals import (
comment_voted,
comment_deleted,
)
from django_comment_client.utils import get_accessible_discussion_modules, is_commentable_cohorted
from django_comment_client.utils import get_accessible_discussion_xblocks, is_commentable_cohorted
from lms.djangoapps.discussion_api.pagination import DiscussionAPIPagination
from lms.lib.comment_client.comment import Comment
from lms.lib.comment_client.thread import Thread
......@@ -215,41 +215,41 @@ def get_courseware_topics(request, course_key, course, topic_ids):
courseware_topics = []
existing_topic_ids = set()
def get_module_sort_key(module):
def get_xblock_sort_key(xblock):
"""
Get the sort key for the module (falling back to the discussion_target
Get the sort key for the xblock (falling back to the discussion_target
setting if absent)
"""
return module.sort_key or module.discussion_target
return xblock.sort_key or xblock.discussion_target
def get_sorted_modules(category):
"""Returns key sorted modules by category"""
return sorted(modules_by_category[category], key=get_module_sort_key)
def get_sorted_xblocks(category):
"""Returns key sorted xblocks by category"""
return sorted(xblocks_by_category[category], key=get_xblock_sort_key)
discussion_modules = get_accessible_discussion_modules(course, request.user)
modules_by_category = defaultdict(list)
for module in discussion_modules:
modules_by_category[module.discussion_category].append(module)
discussion_xblocks = get_accessible_discussion_xblocks(course, request.user)
xblocks_by_category = defaultdict(list)
for xblock in discussion_xblocks:
xblocks_by_category[xblock.discussion_category].append(xblock)
for category in sorted(modules_by_category.keys()):
for category in sorted(xblocks_by_category.keys()):
children = []
for module in get_sorted_modules(category):
if not topic_ids or module.discussion_id in topic_ids:
for xblock in get_sorted_xblocks(category):
if not topic_ids or xblock.discussion_id in topic_ids:
discussion_topic = DiscussionTopic(
module.discussion_id,
module.discussion_target,
get_thread_list_url(request, course_key, [module.discussion_id]),
xblock.discussion_id,
xblock.discussion_target,
get_thread_list_url(request, course_key, [xblock.discussion_id]),
)
children.append(discussion_topic)
if topic_ids and module.discussion_id in topic_ids:
existing_topic_ids.add(module.discussion_id)
if topic_ids and xblock.discussion_id in topic_ids:
existing_topic_ids.add(xblock.discussion_id)
if not topic_ids or children:
discussion_topic = DiscussionTopic(
None,
category,
get_thread_list_url(request, course_key, [item.discussion_id for item in get_sorted_modules(category)]),
get_thread_list_url(request, course_key, [item.discussion_id for item in get_sorted_xblocks(category)]),
children,
)
courseware_topics.append(DiscussionTopicSerializer(discussion_topic).data)
......
......@@ -65,7 +65,7 @@ def _remove_discussion_tab(course, user_id):
"""
Remove the discussion tab for the course.
user_id is passed to the modulestore as the editor of the module.
user_id is passed to the modulestore as the editor of the xblock.
"""
course.tabs = [tab for tab in course.tabs if not tab.type == 'discussion']
modulestore().update_item(course, user_id)
......@@ -206,8 +206,10 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
self.request.user = self.user
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
def make_discussion_module(self, topic_id, category, subcategory, **kwargs):
"""Build a discussion module in self.course"""
def make_discussion_xblock(self, topic_id, category, subcategory, **kwargs):
"""
Build a discussion xblock in self.course.
"""
ItemFactory.create(
parent_location=self.course.location,
category="discussion",
......@@ -274,7 +276,7 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEqual(actual, expected)
def test_with_courseware(self):
self.make_discussion_module("courseware-topic-id", "Foo", "Bar")
self.make_discussion_xblock("courseware-topic-id", "Foo", "Bar")
actual = self.get_course_topics()
expected = {
"courseware_topics": [
......@@ -297,11 +299,11 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
"B": {"id": "non-courseware-2"},
}
self.store.update_item(self.course, self.user.id)
self.make_discussion_module("courseware-1", "A", "1")
self.make_discussion_module("courseware-2", "A", "2")
self.make_discussion_module("courseware-3", "B", "1")
self.make_discussion_module("courseware-4", "B", "2")
self.make_discussion_module("courseware-5", "C", "1")
self.make_discussion_xblock("courseware-1", "A", "1")
self.make_discussion_xblock("courseware-2", "A", "2")
self.make_discussion_xblock("courseware-3", "B", "1")
self.make_discussion_xblock("courseware-4", "B", "2")
self.make_discussion_xblock("courseware-5", "C", "1")
actual = self.get_course_topics()
expected = {
"courseware_topics": [
......@@ -343,13 +345,13 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
"Z": {"id": "non-courseware-4", "sort_key": "W"},
}
self.store.update_item(self.course, self.user.id)
self.make_discussion_module("courseware-1", "First", "A", sort_key="D")
self.make_discussion_module("courseware-2", "First", "B", sort_key="B")
self.make_discussion_module("courseware-3", "First", "C", sort_key="E")
self.make_discussion_module("courseware-4", "Second", "A", sort_key="F")
self.make_discussion_module("courseware-5", "Second", "B", sort_key="G")
self.make_discussion_module("courseware-6", "Second", "C")
self.make_discussion_module("courseware-7", "Second", "D", sort_key="A")
self.make_discussion_xblock("courseware-1", "First", "A", sort_key="D")
self.make_discussion_xblock("courseware-2", "First", "B", sort_key="B")
self.make_discussion_xblock("courseware-3", "First", "C", sort_key="E")
self.make_discussion_xblock("courseware-4", "Second", "A", sort_key="F")
self.make_discussion_xblock("courseware-5", "Second", "B", sort_key="G")
self.make_discussion_xblock("courseware-6", "Second", "C")
self.make_discussion_xblock("courseware-7", "Second", "D", sort_key="A")
actual = self.get_course_topics()
expected = {
......@@ -411,21 +413,21 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
)
with self.store.bulk_operations(self.course.id, emit_signals=False):
self.make_discussion_module("courseware-1", "First", "Everybody")
self.make_discussion_module(
self.make_discussion_xblock("courseware-1", "First", "Everybody")
self.make_discussion_xblock(
"courseware-2",
"First",
"Cohort A",
group_access={self.partition.id: [self.partition.groups[0].id]}
)
self.make_discussion_module(
self.make_discussion_xblock(
"courseware-3",
"First",
"Cohort B",
group_access={self.partition.id: [self.partition.groups[1].id]}
)
self.make_discussion_module("courseware-4", "Second", "Staff Only", visible_to_staff_only=True)
self.make_discussion_module(
self.make_discussion_xblock("courseware-4", "Second", "Staff Only", visible_to_staff_only=True)
self.make_discussion_xblock(
"courseware-5",
"Second",
"Future Start Date",
......@@ -507,8 +509,8 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase):
"""
topic_id_1 = "topic_id_1"
topic_id_2 = "topic_id_2"
self.make_discussion_module(topic_id_1, "test_category_1", "test_target_1")
self.make_discussion_module(topic_id_2, "test_category_2", "test_target_2")
self.make_discussion_xblock(topic_id_1, "test_category_1", "test_target_1")
self.make_discussion_xblock(topic_id_2, "test_category_2", "test_target_2")
actual = get_course_topics(self.request, self.course.id, {"topic_id_1", "topic_id_2"})
self.assertEqual(
actual,
......
......@@ -164,7 +164,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def create_course(self, modules_count, module_store, topics):
"""
Create a course in a specified module store with discussion module and topics
Create a course in a specified module store with discussion xblocks and topics
"""
course = CourseFactory.create(
org="a",
......@@ -176,7 +176,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
)
CourseEnrollmentFactory.create(user=self.user, course_id=course.id)
course_url = reverse("course_topics", kwargs={"course_id": unicode(course.id)})
# add some discussion modules
# add some discussion xblocks
for i in range(modules_count):
ItemFactory.create(
parent_location=course.location,
......@@ -188,9 +188,9 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
)
return course_url
def make_discussion_module(self, topic_id, category, subcategory, **kwargs):
def make_discussion_xblock(self, topic_id, category, subcategory, **kwargs):
"""
Build a discussion module in self.course
Build a discussion xblock in self.course
"""
ItemFactory.create(
parent_location=self.course.location,
......@@ -249,7 +249,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
Tests discussion topic does not exist for the given topic id.
"""
topic_id = "courseware-topic-id"
self.make_discussion_module(topic_id, "test_category", "test_target")
self.make_discussion_xblock(topic_id, "test_category", "test_target")
url = "{}?topic_id=invalid_topic_id".format(self.url)
response = self.client.get(url)
self.assert_response_correct(
......@@ -264,8 +264,8 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"""
topic_id_1 = "topic_id_1"
topic_id_2 = "topic_id_2"
self.make_discussion_module(topic_id_1, "test_category_1", "test_target_1")
self.make_discussion_module(topic_id_2, "test_category_2", "test_target_2")
self.make_discussion_xblock(topic_id_1, "test_category_1", "test_target_1")
self.make_discussion_xblock(topic_id_2, "test_category_2", "test_target_2")
url = "{}?topic_id=topic_id_1,topic_id_2".format(self.url)
response = self.client.get(url)
self.assert_response_correct(
......
......@@ -623,8 +623,8 @@ class SingleThreadContentGroupTestCase(UrlResetMixin, ContentGroupTestCase):
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(course=self.course, text="dummy content", thread_id=thread_id)
for discussion_module in [self.alpha_module, self.beta_module, self.global_module]:
self.assert_can_access(self.staff_user, discussion_module.discussion_id, thread_id, True)
for discussion_xblock in [self.alpha_module, self.beta_module, self.global_module]:
self.assert_can_access(self.staff_user, discussion_xblock.discussion_id, thread_id, True)
def test_alpha_user(self, mock_request):
"""
......@@ -634,8 +634,8 @@ class SingleThreadContentGroupTestCase(UrlResetMixin, ContentGroupTestCase):
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(course=self.course, text="dummy content", thread_id=thread_id)
for discussion_module in [self.alpha_module, self.global_module]:
self.assert_can_access(self.alpha_user, discussion_module.discussion_id, thread_id, True)
for discussion_xblock in [self.alpha_module, self.global_module]:
self.assert_can_access(self.alpha_user, discussion_xblock.discussion_id, thread_id, True)
self.assert_can_access(self.alpha_user, self.beta_module.discussion_id, thread_id, False)
......@@ -647,8 +647,8 @@ class SingleThreadContentGroupTestCase(UrlResetMixin, ContentGroupTestCase):
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(course=self.course, text="dummy content", thread_id=thread_id)
for discussion_module in [self.beta_module, self.global_module]:
self.assert_can_access(self.beta_user, discussion_module.discussion_id, thread_id, True)
for discussion_xblock in [self.beta_module, self.global_module]:
self.assert_can_access(self.beta_user, discussion_xblock.discussion_id, thread_id, True)
self.assert_can_access(self.beta_user, self.alpha_module.discussion_id, thread_id, False)
......
......@@ -269,10 +269,8 @@ def forum_form_discussion(request, course_key):
'threads': json.dumps(threads),
'thread_pages': query_params['num_pages'],
'user_info': json.dumps(user_info, default=lambda x: None),
'can_create_comment': json.dumps(
has_permission(request.user, "create_comment", course.id)),
'can_create_subcomment': json.dumps(
has_permission(request.user, "create_sub_comment", course.id)),
'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
......@@ -381,10 +379,8 @@ def single_thread(request, course_key, discussion_id, thread_id):
'csrf': csrf(request)['csrf_token'],
'init': '', # TODO: What is this?
'user_info': json.dumps(user_info),
'can_create_comment': json.dumps(
has_permission(request.user, "create_comment", course.id)),
'can_create_subcomment': json.dumps(
has_permission(request.user, "create_sub_comment", course.id)),
'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': json.dumps(annotated_content_info),
'course': course,
......
......@@ -177,29 +177,29 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
@ddt.data((ModuleStoreEnum.Type.mongo, 2), (ModuleStoreEnum.Type.split, 1))
@ddt.unpack
def test_get_accessible_discussion_modules(self, modulestore_type, expected_discussion_modules):
def test_get_accessible_discussion_xblocks(self, modulestore_type, expected_discussion_xblocks):
"""
Tests that the accessible discussion modules having no parents do not get fetched for split modulestore.
Tests that the accessible discussion xblocks having no parents do not get fetched for split modulestore.
"""
course = CourseFactory.create(default_store=modulestore_type)
# Create a discussion module.
# Create a discussion xblock.
test_discussion = self.store.create_child(self.user.id, course.location, 'discussion', 'test_discussion')
# Assert that created discussion module is not an orphan.
# Assert that created discussion xblock is not an orphan.
self.assertNotIn(test_discussion.location, self.store.get_orphans(course.id))
# Assert that there is only one discussion module in the course at the moment.
self.assertEqual(len(utils.get_accessible_discussion_modules(course, self.user)), 1)
# Assert that there is only one discussion xblock in the course at the moment.
self.assertEqual(len(utils.get_accessible_discussion_xblocks(course, self.user)), 1)
# Add an orphan discussion module to that course
# Add an orphan discussion xblock to that course
orphan = course.id.make_usage_key('discussion', 'orphan_discussion')
self.store.create_item(self.user.id, orphan.course_key, orphan.block_type, block_id=orphan.block_id)
# Assert that the discussion module is an orphan.
# Assert that the discussion xblock is an orphan.
self.assertIn(orphan, self.store.get_orphans(course.id))
self.assertEqual(len(utils.get_accessible_discussion_modules(course, self.user)), expected_discussion_modules)
self.assertEqual(len(utils.get_accessible_discussion_xblocks(course, self.user)), expected_discussion_xblocks)
@attr('shard_3')
......@@ -262,7 +262,7 @@ class CachedDiscussionIdMapTestCase(ModuleStoreTestCase):
with self.assertRaises(utils.DiscussionIdMapIsNotCached):
utils.get_cached_discussion_key(self.course, 'test_discussion_id')
def test_module_does_not_have_required_keys(self):
def test_xblock_does_not_have_required_keys(self):
self.assertTrue(utils.has_required_keys(self.discussion))
self.assertFalse(utils.has_required_keys(self.bad_discussion))
......@@ -505,7 +505,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
cohorted_if_in_list=True
)
def test_get_unstarted_discussion_modules(self):
def test_get_unstarted_discussion_xblocks(self):
later = datetime.datetime(datetime.MAXYEAR, 1, 1, tzinfo=django_utc())
self.create_discussion("Chapter 1", "Discussion 1", start=later)
......@@ -1026,7 +1026,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
@attr('shard_1')
class ContentGroupCategoryMapTestCase(CategoryMapTestMixin, ContentGroupTestCase):
"""
Tests `get_discussion_category_map` on discussion modules which are
Tests `get_discussion_category_map` on discussion xblocks which are
only visible to some content groups.
"""
def test_staff_user(self):
......
......@@ -108,44 +108,44 @@ def has_forum_access(uname, course_id, rolename):
return role.users.filter(username=uname).exists()
def has_required_keys(module):
def has_required_keys(xblock):
"""
Returns True iff module has the proper attributes for generating metadata
Returns True iff xblock has the proper attributes for generating metadata
with get_discussion_id_map_entry()
"""
for key in ('discussion_id', 'discussion_category', 'discussion_target'):
if getattr(module, key, None) is None:
if getattr(xblock, key, None) is None:
log.debug(
"Required key '%s' not in discussion %s, leaving out of category map",
key,
module.location
xblock.location
)
return False
return True
def get_accessible_discussion_modules(course, user, include_all=False): # pylint: disable=invalid-name
def get_accessible_discussion_xblocks(course, user, include_all=False): # pylint: disable=invalid-name
"""
Return a list of all valid discussion modules in this course that
Return a list of all valid discussion xblocks in this course that
are accessible to the given user.
"""
all_modules = modulestore().get_items(course.id, qualifiers={'category': 'discussion'}, include_orphans=False)
all_xblocks = modulestore().get_items(course.id, qualifiers={'category': 'discussion'}, include_orphans=False)
return [
module for module in all_modules
if has_required_keys(module) and (include_all or has_access(user, 'load', module, course.id))
xblock for xblock in all_xblocks
if has_required_keys(xblock) and (include_all or has_access(user, 'load', xblock, course.id))
]
def get_discussion_id_map_entry(module):
def get_discussion_id_map_entry(xblock):
"""
Returns a tuple of (discussion_id, metadata) suitable for inclusion in the results of get_discussion_id_map().
"""
return (
module.discussion_id,
xblock.discussion_id,
{
"location": module.location,
"title": module.discussion_category.split("/")[-1].strip() + " / " + module.discussion_target
"location": xblock.location,
"title": xblock.discussion_category.split("/")[-1].strip() + " / " + xblock.discussion_target
}
)
......@@ -157,7 +157,7 @@ class DiscussionIdMapIsNotCached(Exception):
def get_cached_discussion_key(course, discussion_id):
"""
Returns the usage key of the discussion module associated with discussion_id if it is cached. If the discussion id
Returns the usage key of the discussion xblock associated with discussion_id if it is cached. If the discussion id
map is cached but does not contain discussion_id, returns None. If the discussion id map is not cached for course,
raises a DiscussionIdMapIsNotCached exception.
"""
......@@ -172,7 +172,7 @@ def get_cached_discussion_key(course, discussion_id):
def get_cached_discussion_id_map(course, discussion_ids, user):
"""
Returns a dict mapping discussion_ids to respective discussion module metadata if it is cached and visible to the
Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the
user. If not, returns the result of get_discussion_id_map
"""
try:
......@@ -181,10 +181,10 @@ def get_cached_discussion_id_map(course, discussion_ids, user):
key = get_cached_discussion_key(course, discussion_id)
if not key:
continue
module = modulestore().get_item(key)
if not (has_required_keys(module) and has_access(user, 'load', module, course.id)):
xblock = modulestore().get_item(key)
if not (has_required_keys(xblock) and has_access(user, 'load', xblock, course.id)):
continue
entries.append(get_discussion_id_map_entry(module))
entries.append(get_discussion_id_map_entry(xblock))
return dict(entries)
except DiscussionIdMapIsNotCached:
return get_discussion_id_map(course, user)
......@@ -192,10 +192,10 @@ def get_cached_discussion_id_map(course, discussion_ids, user):
def get_discussion_id_map(course, user):
"""
Transform the list of this course's discussion modules (visible to a given user) into a dictionary of metadata keyed
Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed
by discussion_id.
"""
return dict(map(get_discussion_id_map_entry, get_accessible_discussion_modules(course, user)))
return dict(map(get_discussion_id_map_entry, get_accessible_discussion_xblocks(course, user)))
def _filter_unstarted_categories(category_map, course):
......@@ -256,7 +256,7 @@ def _sort_map_entries(category_map, sort_alpha):
def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude_unstarted=True):
"""
Transform the list of this course's discussion modules into a recursive dictionary structure. This is used
Transform the list of this course's discussion xblocks into a recursive dictionary structure. This is used
to render the discussion category map in the discussion tab sidebar for a given user.
Args:
......@@ -301,18 +301,21 @@ def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude
"""
unexpanded_category_map = defaultdict(list)
modules = get_accessible_discussion_modules(course, user)
xblocks = get_accessible_discussion_xblocks(course, user)
course_cohort_settings = get_course_cohort_settings(course.id)
for module in modules:
id = module.discussion_id
title = module.discussion_target
sort_key = module.sort_key
category = " / ".join([x.strip() for x in module.discussion_category.split("/")])
# Handle case where module.start is None
entry_start_date = module.start if module.start else datetime.max.replace(tzinfo=pytz.UTC)
unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": entry_start_date})
for xblock in xblocks:
discussion_id = xblock.discussion_id
title = xblock.discussion_target
sort_key = xblock.sort_key
category = " / ".join([x.strip() for x in xblock.discussion_category.split("/")])
# Handle case where xblock.start is None
entry_start_date = xblock.start if xblock.start else datetime.max.replace(tzinfo=pytz.UTC)
unexpanded_category_map[category].append({"title": title,
"id": discussion_id,
"sort_key": sort_key,
"start_date": entry_start_date})
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
for category_path, entries in unexpanded_category_map.items():
......@@ -385,7 +388,7 @@ def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude
return _filter_unstarted_categories(category_map, course) if exclude_unstarted else category_map
def discussion_category_id_access(course, user, discussion_id, module=None):
def discussion_category_id_access(course, user, discussion_id, xblock=None):
"""
Returns True iff the given discussion_id is accessible for user in course.
Assumes that the commentable identified by discussion_id has a null or 'course' context.
......@@ -395,12 +398,12 @@ def discussion_category_id_access(course, user, discussion_id, module=None):
if discussion_id in course.top_level_discussion_topic_ids:
return True
try:
if not module:
if not xblock:
key = get_cached_discussion_key(course, discussion_id)
if not key:
return False
module = modulestore().get_item(key)
return has_required_keys(module) and has_access(user, 'load', module, course.id)
xblock = modulestore().get_item(key)
return has_required_keys(xblock) and has_access(user, 'load', xblock, course.id)
except DiscussionIdMapIsNotCached:
return discussion_id in get_discussion_categories_ids(course, user)
......@@ -417,7 +420,7 @@ def get_discussion_categories_ids(course, user, include_all=False):
"""
accessible_discussion_ids = [
module.discussion_id for module in get_accessible_discussion_modules(course, user, include_all=include_all)
xblock.discussion_id for xblock in get_accessible_discussion_xblocks(course, user, include_all=include_all)
]
return course.top_level_discussion_topic_ids + accessible_discussion_ids
......
......@@ -2,9 +2,11 @@
<%include file="_underscore_templates.html" />
<%!
from django.utils.translation import ugettext as _
from json import dumps as json_dumps
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<div class="discussion-module" data-discussion-id="${discussion_id}" data-user-create-comment="${can_create_comment}" data-user-create-subcomment="${can_create_subcomment}" data-read-only="false">
<div class="discussion-module" data-discussion-id="${discussion_id}" data-user-create-comment="${json_dumps(can_create_comment)}" data-user-create-subcomment="${json_dumps(can_create_subcomment)}" data-read-only="false">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id}" role="button">
<span class="show-hide-discussion-icon"></span><span class="button-text">${_("Show Discussion")}</span>
</a>
......@@ -12,3 +14,15 @@ from django.utils.translation import ugettext as _
<button class="new-post-btn btn-neutral btn-small">${_("Add a Post")}</button>
% endif
</div>
<script type="text/javascript">
/* global DiscussionModuleView */
/* exported DiscussionInlineBlock, $$course_id */
var $$course_id = "${course_id | n, js_escaped_string}";
function DiscussionInlineBlock(runtime, element) {
'use strict';
var el = $(element).find('.discussion-module');
/* jshint nonew:false */
new DiscussionModuleView({ el: el });
}
</script>
<%! from django.utils.translation import ugettext as _ %>
<%page expression_filter="h"/>
<div class="discussion-module" data-discussion-id="${discussion_id | h}">
<div class="discussion-module" data-discussion-id="${discussion_id}">
<p>
<span class="discussion-preview">
<span class="icon fa fa-comment"/>
......
......@@ -32,12 +32,12 @@ class CourseStructureTaskTests(ModuleStoreTestCase):
super(CourseStructureTaskTests, self).setUp()
self.course = CourseFactory.create(org='TestX', course='TS101', run='T1')
self.section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
self.discussion_module_1 = ItemFactory.create(
self.discussion_xblock_1 = ItemFactory.create(
parent=self.course,
category='discussion',
discussion_id='test_discussion_id_1'
)
self.discussion_module_2 = ItemFactory.create(
self.discussion_xblock_2 = ItemFactory.create(
parent=self.course,
category='discussion',
discussion_id='test_discussion_id_2'
......
Open edX: Built-in XBlocks
--------------------------
This area is meant for exceptional and hopefully temporary cases where an
XBlock is integral to the functionality of the Open edX platform.
This is not a pattern we wish for normal XBlocks to follow; they should live in
their own repo.
Discussion XBlock
=================
This XBlock was converted from an XModule, and will hopefully be pulled out of
edx-platform into its own repo at some point. From discussions, it's not too
difficult to move the server-side code , but the client-side code is used by
the discussion board tab and the team discussion, so for now, must remain in
edx-platform.
"""
Setup for discussion-forum XBlock.
"""
import os
from setuptools import setup
def package_data(pkg, root_list):
"""
Generic function to find package_data for `pkg` under `root`.
"""
data = []
for root in root_list:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
setup(
name='xblock-discussion',
version='0.1',
description='XBlock - Discussion',
install_requires=[
'XBlock',
],
entry_points={
'xblock.v1': [
'discussion = xblock_discussion:DiscussionXBlock'
]
},
package_data=package_data("xblock_discussion", ["static"]),
)
# -*- coding: utf-8 -*-
"""
Discussion XBlock
"""
import logging
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblock.core import XBlock
from xblock.fields import Scope, String, UNIQUE_ID
from xblock.fragment import Fragment
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
def _(text):
"""
A noop underscore function that marks strings for extraction.
"""
return text
@XBlock.needs('user')
@XBlock.needs('i18n')
class DiscussionXBlock(XBlock, StudioEditableXBlockMixin):
"""
Provides a discussion forum that is inline with other content in the courseware.
"""
discussion_id = String(scope=Scope.settings, default=UNIQUE_ID, force_export=True)
display_name = String(
display_name=_("Display Name"),
help=_("Display name for this component"),
default="Discussion",
scope=Scope.settings
)
discussion_category = String(
display_name=_("Category"),
default=_("Week 1"),
help=_(
"A category name for the discussion. "
"This name appears in the left pane of the discussion forum for the course."
),
scope=Scope.settings
)
discussion_target = String(
display_name=_("Subcategory"),
default="Topic-Level Student-Visible Label",
help=_(
"A subcategory name for the discussion. "
"This name appears in the left pane of the discussion forum for the course."
),
scope=Scope.settings
)
sort_key = String(scope=Scope.settings)
editable_fields = ["display_name", "discussion_category", "discussion_target"]
has_author_view = True # Tells Studio to use author_view
@property
def course_key(self):
"""
:return: int course id
NB: The goal is to move this XBlock out of edx-platform, and so we use
scope_ids.usage_id instead of runtime.course_id so that the code will
continue to work with workbench-based testing.
"""
return getattr(self.scope_ids.usage_id, 'course_key', None)
@property
def django_user(self):
"""
Returns django user associated with user currently interacting
with the XBlock.
"""
user_service = self.runtime.service(self, 'user')
if not user_service:
return None
return user_service._django_user # pylint: disable=protected-access
def has_permission(self, permission):
"""
Encapsulates lms specific functionality, as `has_permission` is not
importable outside of lms context, namely in tests.
:param user:
:param str permission: Permission
:rtype: bool
"""
# normal import causes the xmodule_assets command to fail due to circular import - hence importing locally
from django_comment_client.permissions import has_permission # pylint: disable=import-error
return has_permission(self.django_user, permission, self.course_key)
def student_view(self, context=None):
"""
Renders student view for LMS.
"""
fragment = Fragment()
course = self.runtime.modulestore.get_course(self.course_key)
context = {
'discussion_id': self.discussion_id,
'user': self.django_user,
'course': course,
'course_id': self.course_key,
'can_create_thread': self.has_permission("create_thread"),
'can_create_comment': self.has_permission("create_comment"),
'can_create_subcomment': self.has_permission("create_subcomment"),
}
fragment.add_content(self.runtime.render_template('discussion/_discussion_inline.html', context))
fragment.initialize_js('DiscussionInlineBlock')
return fragment
def author_view(self, context=None): # pylint: disable=unused-argument
"""
Renders author view for Studio.
"""
fragment = Fragment()
fragment.add_content(self.runtime.render_template(
'discussion/_discussion_inline_studio.html',
{'discussion_id': self.discussion_id}
))
return fragment
def student_view_data(self):
"""
Returns a JSON representation of the student_view of this XBlock.
"""
return {'topic_id': self.discussion_id}
......@@ -8,3 +8,5 @@
-e common/lib/sandbox-packages
-e common/lib/symmath
-e common/lib/xmodule
-e openedx/core/lib/xblock_builtin/xblock_discussion
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