Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 288 additions & 73 deletions xblocks_contrib/discussion/discussion.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,319 @@
"""TO-DO: Write a description of what this XBlock is."""
"""
Discussion XBlock
"""

from importlib.resources import files
import logging
import urllib

from django.utils import translation
import markupsafe
from django.conf import settings
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import get_language_bidi
from web_fragments.fragment import Fragment
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
from xblock.fields import Integer, Scope
from xblock.fields import UNIQUE_ID, Scope, String
from xblock.utils.resources import ResourceLoader
from xblock.utils.studio_editable import StudioEditableXBlockMixin

resource_loader = ResourceLoader(__name__)
from xblocks_contrib.common.xml_utils import LegacyXmlMixin

log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
Text = markupsafe.escape # pylint: disable=invalid-name

# This Xblock is just to test the strucutre of xblocks-contrib
@XBlock.needs("i18n")
class DiscussionXBlock(XBlock):

def _(text):
"""
TO-DO: document what your XBlock does.
A noop underscore function that marks strings for extraction.
"""
return text

# Fields are defined on the class. You can access them in your code as
# self.<fieldname>.

# TO-DO: delete count, and define your own fields.
count = Integer(
default=0,
scope=Scope.user_state,
help="A simple counter, to show something happening",
)
def HTML(html): # pylint: disable=invalid-name
"""
Mark a string as already HTML, so that it won't be escaped before output.

Use this function when formatting HTML into other strings. It must be
used in conjunction with ``Text()``, and both ``HTML()`` and ``Text()``
must be closed before any calls to ``format()``::

<%page expression_filter="h"/>
<%!
from django.utils.translation import gettext as _

from openedx.core.djangolib.markup import HTML, Text
%>
${Text(_("Write & send {start}email{end}")).format(
start=HTML("<a href='mailto:{}'>").format(user.email),
end=HTML("</a>"),
)}

"""
return markupsafe.Markup(html)


def is_discussion_enabled(course_id): # pylint: disable=unused-argument
"""
Return True if discussions are enabled; else False
"""
return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE')


# Indicates that this XBlock has been extracted from edx-platform.
@XBlock.needs("i18n")
@XBlock.wants("user")
# pylint: disable=abstract-method
class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, LegacyXmlMixin):
"""
Provides a discussion forum that is inline with other content in the courseware.
"""
is_extracted = True
completion_mode = XBlockCompletionMode.EXCLUDED

discussion_id = String(scope=Scope.settings, default=UNIQUE_ID)
display_name = String(
display_name=_("Display Name"),
help=_("The 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 getattr(self.scope_ids.usage_id, 'course_key', None)

def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
return files(__package__).joinpath(path).read_text(encoding="utf-8")
@property
def is_visible(self):
"""
Discussion Xblock does not support new OPEN_EDX provider
"""
# TO-DO: Need to fix import issues
# provider = DiscussionsConfiguration.get(self.course_key)
# return provider.provider_type == Provider.LEGACY
return True

@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 get_all_js_files(self):
"""
Returns list of all JS files in the correct dependency order.
"""
return [
# Vendor files (load first as dependencies)
'static/js/vendor/Markdown.Converter.js',
'static/js/vendor/Markdown.Sanitizer.js',
'static/js/vendor/Markdown.Editor.js',
'static/js/vendor/jquery.ajaxfileupload.js',
'static/js/vendor/jquery.timeago.js',
'static/js/vendor/jquery.timeago.locale.js',
'static/js/vendor/jquery.truncate.js',
'static/js/vendor/split.js',
# MathJax utilities
'static/js/mathjax_accessible.js',
'static/js/mathjax_delay_renderer.js',
# Core utilities and models
'static/js/common/utils.js',
'static/js/common/models/discussion_course_settings.js',
'static/js/common/models/discussion_user.js',
# Core discussion functionality
# content.js must come before discussion.js because discussion.js uses Thread
'static/js/common/content.js',
'static/js/common/discussion.js',
'static/js/common/mathjax_include.js',
# Custom WMD editor
'static/js/customwmd.js',
# Views (depend on core discussion and models)
'static/js/common/views/discussion_content_view.js',
'static/js/common/views/discussion_inline_view.js',
'static/js/common/views/discussion_thread_edit_view.js',
'static/js/common/views/discussion_thread_list_view.js',
'static/js/common/views/discussion_thread_profile_view.js',
'static/js/common/views/discussion_thread_show_view.js',
'static/js/common/views/discussion_thread_view.js',
'static/js/common/views/discussion_topic_menu_view.js',
'static/js/common/views/new_post_view.js',
'static/js/common/views/response_comment_edit_view.js',
'static/js/common/views/response_comment_show_view.js',
'static/js/common/views/response_comment_view.js',
'static/js/common/views/thread_response_edit_view.js',
'static/js/common/views/thread_response_show_view.js',
'static/js/common/views/thread_response_view.js',
]

def add_resource_urls(self, fragment):
"""
Adds URLs for JS and CSS resources that this XBlock depends on to `fragment`.
"""

css_file_path = (
'static/css/inline-discussion-rtl.css'
if get_language_bidi()
else 'static/css/inline-discussion.css'
)
fragment.add_css(loader.load_unicode(css_file_path))

# Load all JS files individually in the correct order
for js_file in self.get_all_js_files():
fragment.add_javascript(loader.load_unicode(js_file))

def has_permission(self, permission): # pylint: disable=unused-argument
"""
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
"""
# TO-DO: Need to fix import issues
# return has_permission(self.django_user, permission, self.course_key)
return True

# TO-DO: change this view to display your data your own way.
def student_view(self, context=None):
"""
Create primary view of the DiscussionXBlock, shown to students when viewing courses.
Renders student view for LMS.
"""
if context:
pass # TO-DO: do something based on the context.

frag = Fragment()
frag.add_content(
resource_loader.render_django_template(
"templates/discussion.html",
{
"count": self.count,
},
i18n_service=self.runtime.service(self, "i18n"),
fragment = Fragment()

if not self.is_visible:
return fragment

self.add_resource_urls(fragment)
login_msg = ''

if not self.django_user.is_authenticated:
qs = urllib.parse.urlencode({
'course_id': self.course_key,
'enrollment_action': 'enroll',
'email_opt_in': False,
})
login_msg = Text(_("You are not signed in. To view the discussion content, {sign_in_link} or "
"{register_link}, and enroll in this course.")).format(
sign_in_link=HTML('<a href="{url}">{sign_in_label}</a>').format(
sign_in_label=_('sign in'),
url='{}?{}'.format(reverse('signin_user'), qs),
),
register_link=HTML('<a href="/{url}">{register_label}</a>').format(
register_label=_('register'),
url='{}?{}'.format(reverse('register_user'), qs),
),
)

if is_discussion_enabled(self.course_key):
context = {
'discussion_id': self.discussion_id,
'display_name': self.display_name if self.display_name else _("Discussion"),
'user': self.django_user,
'course_id': self.course_key,
'discussion_category': self.discussion_category,
'discussion_target': self.discussion_target,
'can_create_thread': self.has_permission("create_thread"),
'can_create_comment': self.has_permission("create_comment"),
'can_create_subcomment': self.has_permission("create_sub_comment"),
'login_msg': login_msg,
}
fragment.add_content(
render_to_string('discussion/_discussion_inline.html', context)
)

fragment.initialize_js('DiscussionInlineBlock')

return fragment

def author_view(self, context=None):
"""
Renders author view for Studio.
"""
fragment = Fragment()
context = {
'discussion_id': self.discussion_id,
'is_visible': self.is_visible,
}
fragment.add_content(
loader.render_django_template('templates/_discussion_inline_studio.html', context)
)
return fragment

frag.add_css(self.resource_string("static/css/discussion.css"))
frag.add_javascript(self.resource_string("static/js/src/discussion.js"))
frag.initialize_js("DiscussionXBlock")
return frag
def student_view_data(self):
"""
Returns a JSON representation of the student_view of this XBlock.
"""
return {'topic_id': self.discussion_id}

# TO-DO: change this handler to perform your own actions. You may need more
# than one handler, or you may not need any handlers at all.
@XBlock.json_handler
def increment_count(self, data, suffix=""):
@classmethod
def parse_xml(cls, node, runtime, keys):
"""
Increments data. An example handler.
Parses OLX into XBlock.

This method is overridden here to allow parsing legacy OLX, coming from discussion XModule.
XBlock stores all the associated data, fields and children in a XML element inlined into vertical XML file
XModule stored only minimal data on the element included into vertical XML and used a dedicated "discussion"
folder in OLX to store fields and children. Also, some info was put into "policy.json" file.

If no external data sources are found (file in "discussion" folder), it is exactly equivalent to base method
XBlock.parse_xml. Otherwise this method parses file in "discussion" folder (known as definition_xml), applies
policy.json and updates fields accordingly.
"""
if suffix:
pass # TO-DO: Use the suffix when storing data.
# Just to show data coming in...
assert data["hello"] == "world"
block = super().parse_xml(node, runtime, keys)

self.count += 1
return {"count": self.count}
cls._apply_metadata_and_policy(block, node, runtime)

# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
@staticmethod
def workbench_scenarios():
"""Create canned scenario for display in the workbench."""
return [
(
"DiscussionXBlock",
"""<_discussion_extracted/>
""",
),
(
"Multiple DiscussionXBlock",
"""<vertical_demo>
<_discussion_extracted/>
<_discussion_extracted/>
<_discussion_extracted/>
</vertical_demo>
""",
),
]
return block

@staticmethod
def get_dummy():
@classmethod
def _apply_metadata_and_policy(cls, block, node, runtime):
"""
Generate initial i18n with dummy method.
Attempt to load definition XML from "discussion" folder in OLX, than parse it and update block fields
"""
return translation.gettext_noop("Dummy")
if node.get('url_name') is None:
return # Newer/XBlock XML format - no need to load an additional file.
try:
definition_xml, _ = cls.load_definition_xml(node, runtime, block.scope_ids.def_id)
except Exception as err: # pylint: disable=broad-except
log.info(
"Exception %s when trying to load definition xml for block %s - assuming XBlock export format",
err,
block
)
return

metadata = cls.load_metadata(definition_xml)
cls.apply_policy(metadata, runtime.get_policy(block.scope_ids.usage_id))

for field_name, value in metadata.items():
if field_name in block.fields:
setattr(block, field_name, value)
Loading