Commit 382c2edf by E. Kolpakov Committed by Jonathan Piacenti

Moved discussion XBlock to edx-platform

(cherry picked from commit a7c0a2a)
parent 1ce71942
...@@ -38,14 +38,6 @@ GRADER_TYPES = { ...@@ -38,14 +38,6 @@ GRADER_TYPES = {
} }
# Add Discussion templates
add_lookup('lms.main', 'templates', package='discussion_app')
# Add Discussion templates
add_lookup('lms.main', 'templates', package='discussion_app')
# points to the temporary course landing page with log in and sign up # points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename): def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {}) return render_to_response('temp-course-landing.html', {})
......
...@@ -17,6 +17,3 @@ def run(): ...@@ -17,6 +17,3 @@ def run():
clear_lookups(namespace) clear_lookups(namespace)
for directory in directories: for directory in directories:
add_lookup(namespace, directory) add_lookup(namespace, directory)
# Add Discussion templates
add_lookup('main', 'templates', package='discussion_app')
from .discussion_forum import DiscussionXBlock, DiscussionCourseXBlock
import logging
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from .utils import (
render_template,
render_mako_template,
render_mustache_templates,
get_js_urls, get_css_urls,
asset_to_static_url
)
log = logging.getLogger(__name__)
@XBlock.needs('discussion')
class DiscussionXBlock(XBlock):
""" Provides functionality similar to discussion XModule in inline mode """
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)
@property
def discussion_id(self):
"""
:return: int discussion id
"""
return self.scope_ids.usage_id.block_id
@property
def course_id(self):
"""
:return: int course id
"""
# TODO really implement this
# pylint: disable=no-member
if hasattr(self, 'xmodule_runtime'):
if hasattr(self.xmodule_runtime.course_id, 'to_deprecated_string'):
return self.xmodule_runtime.course_id.to_deprecated_string()
else:
return self.xmodule_runtime.course_id
return 'foo'
def student_view(self, context=None): # pylint: disable=unused-argument
""" Renders student view for LMS and Studio """
# pylint: disable=no-member
if hasattr(self, 'xmodule_runtime') and getattr(self.xmodule_runtime, 'is_author_mode', False):
fragment = self._student_view_studio()
else:
fragment = self._student_view_lms()
return fragment
def _student_view_lms(self):
""" Renders student view for LMS """
fragment = Fragment()
discussion_service = self.xmodule_runtime.service(self, 'discussion') # pylint: disable=no-member
context = discussion_service.get_inline_template_context(self.discussion_id)
context['discussion_id'] = self.discussion_id
fragment.add_content(render_mako_template('discussion/_discussion_inline.html', context))
for url in get_js_urls():
fragment.add_javascript_url(url)
for url in get_css_urls():
fragment.add_css_url(url)
fragment.add_javascript(render_template('static/js/discussion_inline.js', {'course_id': self.course_id}))
fragment.add_content(render_mustache_templates())
fragment.initialize_js('DiscussionInlineBlock')
return fragment
def _student_view_studio(self):
""" Renders student view for Studio """
fragment = Fragment()
fragment.add_content(render_mako_template(
'discussion/_discussion_inline_studio.html',
{'discussion_id': self.discussion_id}
))
fragment.add_css_url(asset_to_static_url('xblock/discussion/css/discussion-studio.css'))
return fragment
@XBlock.json_handler
def studio_submit(self, data, suffix=''): # pylint: disable=unused-argument
""" Handles Studio submit event """
log.info("submitted: {}".format(data))
self.display_name = data.get("display_name", "Untitled Discussion Topic")
self.discussion_category = data.get("discussion_category", None)
self.discussion_target = data.get("discussion_target", None)
return {
"display_name": self.display_name,
"discussion_category": self.discussion_category,
"discussion_target": self.discussion_target
}
def studio_view(self, context=None): # pylint: disable=unused-argument
""" Renders author view for Studio """
fragment = Fragment()
context = {
"display_name": self.display_name,
"discussion_category": self.discussion_category,
"discussion_target": self.discussion_target
}
log.info("rendering template in context: {}".format(context))
fragment.add_content(render_mako_template('discussion/discussion_inline_edit.html', context))
fragment.add_javascript_url(asset_to_static_url('xblock/discussion/js/discussion_inline_edit.js'))
fragment.initialize_js('DiscussionEditBlock')
return fragment
@staticmethod
def workbench_scenarios():
"""A canned scenario for display in the workbench."""
return [
("Discussion XBlock",
"""<vertical_demo>
<discussion-forum/>
</vertical_demo>
"""),
]
@XBlock.needs('discussion')
class DiscussionCourseXBlock(XBlock):
""" Provides functionality similar to discussion XModule in tab mode """
display_name = String(
display_name="Display Name",
help="Display name for this module",
default="Discussion Course",
scope=Scope.settings
)
def student_view(self, context=None): # pylint: disable=unused-argument
""" Renders student view for LMS and Studio """
# pylint: disable=no-member
if hasattr(self, 'xmodule_runtime') and getattr(self.xmodule_runtime, 'is_author_mode', False):
fragment = self._student_view_studio()
else:
fragment = self._student_view_lms()
return fragment
def _student_view_lms(self):
""" Renders student view for LMS """
fragment = Fragment()
fragment.add_css_url(asset_to_static_url('xblock/discussion/css/discussion-course-custom.css'))
discussion_service = self.xmodule_runtime.service(self, 'discussion') # pylint: disable=no-member
context = discussion_service.get_course_template_context()
context['enable_new_post_btn'] = True
fragment.add_content(render_mako_template('discussion/_discussion_course.html', context))
fragment.add_javascript(render_template('static/js/discussion_course.js', {
'course_id': self.course_id
}))
fragment.add_content(render_mustache_templates())
for url in get_js_urls():
fragment.add_javascript_url(url)
for url in get_css_urls():
fragment.add_css_url(url)
fragment.initialize_js('DiscussionCourseBlock')
return fragment
def _student_view_studio(self):
""" Renders student view for Studio """
fragment = Fragment()
fragment.add_content(render_mako_template('discussion/_discussion_course_studio.html'))
fragment.add_css_url(asset_to_static_url('xblock/discussion/css/discussion-studio.css'))
return fragment
def studio_view(self, context=None): # pylint: disable=unused-argument
""" Renders author view Studio """
return Fragment()
var $$course_id = "{{course_id}}";
function DiscussionCourseBlock(runtime, element) {
var el = $(element).find('section.discussion');
el.data('pushState', 'false');
var testUrl = runtime.handlerUrl(element, 'test');
if (testUrl.match(/^(http|https):\/\//)) {
var hostname = testUrl.match(/^(.*:\/\/[a-z\-.]+)\//)[1];
DiscussionUtil.setBaseUrl(hostname);
}
}
var $$course_id = "{{course_id}}";
function DiscussionInlineBlock(runtime, element) {
var el = $(element).find('.discussion-module');
var testUrl = runtime.handlerUrl(element, 'test');
if (testUrl.match(/^(http|https):\/\//)) {
var hostname = testUrl.match(/^(.*:\/\/[a-z\-.]+)\//)[1];
DiscussionUtil.setBaseUrl(hostname);
}
new DiscussionModuleView({
el: el,
async_thread_views: true
});
}
import os
import pkg_resources
from django.templatetags.static import static
from edxmako.shortcuts import render_to_string
from django.template import Context, Template
from django.conf import settings
from mako.template import Template as MakoTemplate
JS_URLS = [
# VENDOR
'js/vendor/URI.min.js',
'js/vendor/jquery.leanModal.min.js',
'js/vendor/jquery.timeago.js',
'js/vendor/underscore-min.js',
'js/vendor/backbone-min.js',
'js/vendor/mustache.js',
'xblock/discussion/js/vendor/split.js',
'xblock/discussion/js/vendor/i18n.js',
'xblock/discussion/js/vendor/Markdown.Converter.js',
'xblock/discussion/js/vendor/Markdown.Sanitizer.js',
'xblock/discussion/js/vendor/Markdown.Editor.js',
'xblock/discussion/js/vendor/mathjax_delay_renderer.js',
'xblock/discussion/js/vendor/customwmd.js',
]
CSS_URLS = [
# 'xblock/discussion/css/discussion-app.css',
'xblock/discussion/css/vendor/font-awesome.css'
]
def load_resource(resource_path):
"""
Gets the content of a resource
"""
resource_content = pkg_resources.resource_string('discussion_forum', resource_path)
return unicode(resource_content)
def render_template(template_path, context=None):
"""
Evaluate a template by resource path, applying the provided context
"""
template_str = load_resource(template_path)
template = Template(template_str)
return template.render(Context(context if context else {}))
def render_mako_template(template_path, context=None):
"""
Evaluate a mako template by resource path, applying the provided context
"""
return render_to_string(template_path, context if context else {})
def render_mustache_templates():
""" Renders all mustache templates as script tags """
mustache_dir = settings.COMMON_ROOT / 'templates' / 'discussion' / 'mustache'
def is_valid_file_name(file_name):
""" Checks if file is a mustache template """
return file_name.endswith('.mustache')
def read_file(file_name):
""" Reads file and decodes it's content """
return open(mustache_dir + '/' + file_name, "r").read().decode('utf-8')
def template_id_from_file_name(file_name):
""" Generates template_id from file name """
return file_name.rpartition('.')[0]
def process_mako(template_content):
""" Creates and renders Mako template """
return MakoTemplate(template_content).render_unicode()
def make_script_tag(script_id, content):
""" Wraps content in script tag """
return u"<script type='text/template' id='{0}'>{1}</script>".format(script_id, content)
return u'\n'.join(
make_script_tag(template_id_from_file_name(file_name), process_mako(read_file(file_name)))
for file_name in os.listdir(mustache_dir)
if is_valid_file_name(file_name)
)
def get_scenarios_from_path(scenarios_path, include_identifier=False):
"""
Returns an array of (title, xmlcontent) from files contained in a specified directory,
formatted as expected for the return value of the workbench_scenarios() method
"""
base_fullpath = os.path.dirname(os.path.realpath(__file__))
scenarios_fullpath = os.path.join(base_fullpath, scenarios_path)
scenarios = []
if os.path.isdir(scenarios_fullpath):
for template in os.listdir(scenarios_fullpath):
if not template.endswith('.xml'):
continue
identifier = template[:-4]
title = identifier.replace('_', ' ').title()
template_path = os.path.join(scenarios_path, template)
if not include_identifier:
scenarios.append((title, load_resource(template_path)))
else:
scenarios.append((identifier, title, load_resource(template_path)))
return scenarios
def load_scenarios_from_path(scenarios_path):
"""
Load all xml files contained in a specified directory, as workbench scenarios
"""
return get_scenarios_from_path(scenarios_path, include_identifier=True)
def get_js_urls():
""" Returns a list of all additional javascript files """
return [asset_to_static_url(path) for path in JS_URLS]
def get_css_urls():
""" Returns a list of all additional css files """
return [asset_to_static_url(path) for path in CSS_URLS]
def asset_to_static_url(asset_path):
"""
:param str asset_path: path to asset
:return: str|unicode url of asset
"""
return static(asset_path)
"""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 Forum',
packages=[
'discussion_forum'
],
install_requires=[
'XBlock',
],
entry_points={
'xblock.v1': [
'discussion-forum = discussion_forum:DiscussionXBlock',
'discussion-course = discussion_forum:DiscussionCourseXBlock'
]
},
package_data=package_data("discussion_forum", ["static"]),
)
...@@ -59,9 +59,9 @@ class DiscussionModule(DiscussionFields, XModule): ...@@ -59,9 +59,9 @@ class DiscussionModule(DiscussionFields, XModule):
'course': self.get_course(), 'course': self.get_course(),
} }
if getattr(self.system, 'is_author_mode', False): if getattr(self.system, 'is_author_mode', False):
template = '/discussion/_discussion_inline_studio.html' template = 'discussion/_discussion_inline_studio.html'
else: else:
template = '/discussion/_discussion_inline.html' template = 'discussion/_discussion_inline.html'
return self.system.render_template(template, context) return self.system.render_template(template, context)
def get_course(self): def get_course(self):
......
...@@ -1404,21 +1404,27 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p ...@@ -1404,21 +1404,27 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
return result return result
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
# Currently, Modulestore is responsible for instantiating DescriptorSystems if block.xmodule_runtime is not None:
# This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem return block.xmodule_runtime.handler_url(block, handler_name, suffix, query, thirdparty)
# that implements the correct handler url. So, for now, instead, we will reference a else:
# global function that the application can override. # Currently, Modulestore is responsible for instantiating DescriptorSystems
return descriptor_global_handler_url(block, handler_name, suffix, query, thirdparty) # This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem
# that implements the correct handler url. So, for now, instead, we will reference a
# global function that the application can override.
return descriptor_global_handler_url(block, handler_name, suffix, query, thirdparty)
def local_resource_url(self, block, uri): def local_resource_url(self, block, uri):
""" """
See :meth:`xblock.runtime.Runtime:local_resource_url` for documentation. See :meth:`xblock.runtime.Runtime:local_resource_url` for documentation.
""" """
# Currently, Modulestore is responsible for instantiating DescriptorSystems if block.xmodule_runtime is not None:
# This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem return block.xmodule_runtime.local_resource_url(block, uri)
# that implements the correct local_resource_url. So, for now, instead, we will reference a else:
# global function that the application can override. # Currently, Modulestore is responsible for instantiating DescriptorSystems
return descriptor_global_local_resource_url(block, uri) # This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem
# that implements the correct local_resource_url. So, for now, instead, we will reference a
# global function that the application can override.
return descriptor_global_local_resource_url(block, uri)
def applicable_aside_types(self, block): def applicable_aside_types(self, block):
""" """
...@@ -1436,6 +1442,13 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p ...@@ -1436,6 +1442,13 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
""" """
raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls") raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls")
def publish(self, block, event_type, event):
"""
See :meth:`xblock.runtime.Runtime:publish` for documentation.
"""
if block.xmodule_runtime is not None:
return block.xmodule_runtime.publish(block, event_type, event)
def add_block_as_child_node(self, block, node): def add_block_as_child_node(self, block, node):
child = etree.SubElement(node, "unknown") child = etree.SubElement(node, "unknown")
child.set('url_name', block.url_name) child.set('url_name', block.url_name)
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
testCancel = function(view) { testCancel = function(view) {
view.$('.post-cancel').click(); view.$('.post-cancel').click();
expect($('.edit-post-form')).not.toExist(); expect($('.edit-post-form')).not.toExist();
} };
it('can close the view in tab mode', function() { it('can close the view in tab mode', function() {
this.createEditView(); this.createEditView();
......
...@@ -11,7 +11,7 @@ if Backbone? ...@@ -11,7 +11,7 @@ if Backbone?
paginationTemplate: -> DiscussionUtil.getTemplate("_pagination") paginationTemplate: -> DiscussionUtil.getTemplate("_pagination")
page_re: /\?discussion_page=(\d+)/ page_re: /\?discussion_page=(\d+)/
initialize: -> initialize: (options) =>
@toggleDiscussionBtn = @$(".discussion-show") @toggleDiscussionBtn = @$(".discussion-show")
# Set the page if it was set in the URL. This is used to allow deep linking to pages # Set the page if it was set in the URL. This is used to allow deep linking to pages
match = @page_re.exec(window.location.href) match = @page_re.exec(window.location.href)
...@@ -19,6 +19,7 @@ if Backbone? ...@@ -19,6 +19,7 @@ if Backbone?
@page = parseInt(match[1]) @page = parseInt(match[1])
else else
@page = 1 @page = 1
@options = options
toggleNewPost: (event) => toggleNewPost: (event) =>
event.preventDefault() event.preventDefault()
...@@ -96,8 +97,8 @@ if Backbone? ...@@ -96,8 +97,8 @@ if Backbone?
@$('section.discussion').replaceWith($discussion) @$('section.discussion').replaceWith($discussion)
else else
@$el.append($discussion) @$el.append($discussion)
@newPostForm = this.$el.find('.new-post-article') @newPostForm = this.$el.find('.new-post-article')
async = @options.async_thread_views
@threadviews = @discussion.map (thread) => @threadviews = @discussion.map (thread) =>
view = new DiscussionThreadView( view = new DiscussionThreadView(
el: @$("article#thread_#{thread.id}"), el: @$("article#thread_#{thread.id}"),
......
if Backbone? if Backbone?
DiscussionApp = DiscussionApp =
start: (elem)-> start: (elem, pushState = true)->
# TODO: Perhaps eliminate usage of global variables when possible # TODO: Perhaps eliminate usage of global variables when possible
DiscussionUtil.loadRolesFromContainer() DiscussionUtil.loadRolesFromContainer()
element = $(elem) element = $(elem)
...@@ -17,7 +17,7 @@ if Backbone? ...@@ -17,7 +17,7 @@ if Backbone?
discussion = new Discussion(threads, {pages: thread_pages, sort: sort_preference}) discussion = new Discussion(threads, {pages: thread_pages, sort: sort_preference})
course_settings = new DiscussionCourseSettings(element.data("course-settings")) course_settings = new DiscussionCourseSettings(element.data("course-settings"))
new DiscussionRouter({discussion: discussion, course_settings: course_settings}) new DiscussionRouter({discussion: discussion, course_settings: course_settings})
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"}) Backbone.history.start({pushState: pushState, root: "/courses/#{$$course_id}/discussion/forum/"})
DiscussionProfileApp = DiscussionProfileApp =
start: (elem) -> start: (elem) ->
# Roles are not included in user profile page, but they are not used for anything # Roles are not included in user profile page, but they are not used for anything
...@@ -32,6 +32,7 @@ if Backbone? ...@@ -32,6 +32,7 @@ if Backbone?
new DiscussionUserProfileView(el: element, collection: threads, page: page, numPages: numPages) new DiscussionUserProfileView(el: element, collection: threads, page: page, numPages: numPages)
$ -> $ ->
$("section.discussion").each (index, elem) -> $("section.discussion").each (index, elem) ->
DiscussionApp.start(elem) pushState = $(elem).data('pushState') != 'false'
DiscussionApp.start(elem, pushState)
$("section.discussion-user-threads").each (index, elem) -> $("section.discussion-user-threads").each (index, elem) ->
DiscussionProfileApp.start(elem) DiscussionProfileApp.start(elem)
...@@ -22,6 +22,8 @@ if Backbone? ...@@ -22,6 +22,8 @@ if Backbone?
if @mode not in ["tab", "inline"] if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode) throw new Error("invalid mode: " + @mode)
@async_thread_views = if options.async_thread_views? then options.async_thread_views else false
# Quick fix to have an actual model when we're receiving new models from # Quick fix to have an actual model when we're receiving new models from
# the server. # the server.
@model.collection.on "reset", (collection) => @model.collection.on "reset", (collection) =>
...@@ -231,7 +233,7 @@ if Backbone? ...@@ -231,7 +233,7 @@ if Backbone?
renderResponseToList: (response, listSelector, options) => renderResponseToList: (response, listSelector, options) =>
response.set('thread', @model) response.set('thread', @model)
view = new ThreadResponseView($.extend({model: response}, options)) view = new ThreadResponseView($.extend({model: response, async: @async_thread_views}, options))
view.on "comment:add", @addComment view.on "comment:add", @addComment
view.on "comment:endorse", @endorseThread view.on "comment:endorse", @endorseThread
view.render() view.render()
...@@ -265,7 +267,6 @@ if Backbone? ...@@ -265,7 +267,6 @@ if Backbone?
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id")) comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id"))
comment.set('thread', @model.get('thread')) comment.set('thread', @model.get('thread'))
@renderResponseToList(comment, ".js-response-list") @renderResponseToList(comment, ".js-response-list")
@renderAttrs()
@model.addComment() @model.addComment()
@renderAddResponseButton() @renderAddResponseButton()
......
...@@ -12,6 +12,7 @@ if Backbone? ...@@ -12,6 +12,7 @@ if Backbone?
initialize: (options) -> initialize: (options) ->
@collapseComments = options.collapseComments @collapseComments = options.collapseComments
@async = if options.async? then options.async else false;
@createShowView() @createShowView()
renderTemplate: -> renderTemplate: ->
...@@ -199,7 +200,7 @@ if Backbone? ...@@ -199,7 +200,7 @@ if Backbone?
url: url url: url
type: "POST" type: "POST"
dataType: 'json' dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly.. async: @async # TODO when the rest of the stuff below is made to work properly..
data: data:
body: newBody body: newBody
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors")) error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
......
/* Some fixes that should only be applied when using discussion as an xblock */
.discussion-body .sort-bar li {
margin: 4px 0px 0 0 !important;
}
.discussion-body .post-list a .title {
width: 60% !important;
}
div.discussion-body div.forum-nav div.forum-nav-thread-list-wrapper ul.forum-nav-thread-list,
div.discussion-body div.forum-nav div.forum-nav-browse-menu-wrapper ul.forum-nav-browse-menu,
div.discussion-body div.forum-nav div.forum-nav-thread-list-wrapper ul.forum-nav-thread-list ul
{
padding-left: 0;
list-style: none;
}
div.discussion-body div.forum-nav div.forum-nav-browse-menu-wrapper ul.forum-nav-browse-submenu {
list-style: none
}
div.discussion-body div.forum-nav div.forum-nav-thread-list-wrapper ul.forum-nav-thread-list li {
margin-bottom: 0;
}
.discussion-module {
display: block;
border: none;
box-shadow: none;
line-height: 1.4;
position: relative;
margin: 20px 0;
padding: 20px;
background: #f6f6f6;
border-radius: 3px;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
function DiscussionEditBlock(runtime, element) {
$('.save-button').bind('click', function() {
var data = {
'display_name': $('#display-name').val(),
'discussion_category': $('#discussion-category').val(),
'discussion_target': $('#discussion-target').val()
};
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
$.post(handlerUrl, JSON.stringify(data)).complete(function() {
window.location.reload(false);
});
});
$('.cancel-button').bind('click', function() {
runtime.notify('cancel', {});
});
}
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
return tag;
else
return "";
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();
// Generated by CoffeeScript 1.6.1
(function() {
$(function() {
var HUB, MathJaxProcessor;
if (typeof MathJax === "undefined" || MathJax === null) {
return;
}
HUB = MathJax.Hub;
MathJaxProcessor = (function() {
var CODESPAN, MATHSPLIT;
MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i;
CODESPAN = /(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm;
function MathJaxProcessor(inlineMark, displayMark) {
this.inlineMark = inlineMark || "$";
this.displayMark = displayMark || "$$";
this.math = null;
this.blocks = null;
}
MathJaxProcessor.prototype.processMath = function(start, last, preProcess) {
var block, i, _i, _ref;
block = this.blocks.slice(start, last + 1).join("").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (HUB.Browser.isMSIE) {
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n");
}
for (i = _i = _ref = start + 1; _ref <= last ? _i <= last : _i >= last; i = _ref <= last ? ++_i : --_i) {
this.blocks[i] = "";
}
this.blocks[start] = "@@" + this.math.length + "@@";
if (preProcess) {
block = preProcess(block);
}
return this.math.push(block);
};
MathJaxProcessor.prototype.removeMath = function(text) {
var block, braces, current, deTilde, end, hasCodeSpans, last, start, _i, _ref;
text = text || "";
this.math = [];
start = end = last = null;
braces = 0;
hasCodeSpans = /`/.test(text);
if (hasCodeSpans) {
text = text.replace(/~/g, "~T").replace(CODESPAN, function($0) {
return $0.replace(/\$/g, "~D");
});
deTilde = function(text) {
return text.replace(/~([TD])/g, function($0, $1) {
return {
T: "~",
D: "$"
}[$1];
});
};
} else {
deTilde = function(text) {
return text;
};
}
this.blocks = _split(text.replace(/\r\n?/g, "\n"), MATHSPLIT);
for (current = _i = 1, _ref = this.blocks.length; _i < _ref; current = _i += 2) {
block = this.blocks[current];
if (block.charAt(0) === "@") {
this.blocks[current] = "@@" + this.math.length + "@@";
this.math.push(block);
} else if (start) {
if (block === end) {
if (braces) {
last = current;
} else {
this.processMath(start, current, deTilde);
start = end = last = null;
}
} else if (block.match(/\n.*\n/)) {
if (last) {
current = last;
this.processMath(start, current, deTilde);
}
start = end = last = null;
braces = 0;
} else if (block === "{") {
++braces;
} else if (block === "}" && braces) {
--braces;
}
} else {
if (block === this.inlineMark || block === this.displayMark) {
start = current;
end = block;
braces = 0;
} else if (block.substr(1, 5) === "begin") {
start = current;
end = "\\end" + block.substr(6);
braces = 0;
}
}
}
if (last) {
this.processMath(start, last, deTilde);
start = end = last = null;
}
return deTilde(this.blocks.join(""));
};
MathJaxProcessor.removeMathWrapper = function(_this) {
return function(text) {
return _this.removeMath(text);
};
};
MathJaxProcessor.prototype.replaceMath = function(text) {
var _this = this;
text = text.replace(/@@(\d+)@@/g, function($0, $1) {
return _this.math[$1];
});
this.math = null;
return text;
};
MathJaxProcessor.replaceMathWrapper = function(_this) {
return function(text) {
return _this.replaceMath(text);
};
};
return MathJaxProcessor;
})();
if (typeof Markdown !== "undefined" && Markdown !== null) {
Markdown.getMathCompatibleConverter = function(postProcessor) {
var converter, processor;
postProcessor || (postProcessor = (function(text) {
return text;
}));
converter = Markdown.getSanitizingConverter();
processor = new MathJaxProcessor();
converter.hooks.chain("preConversion", MathJaxProcessor.removeMathWrapper(processor));
converter.hooks.chain("postConversion", function(text) {
return postProcessor(MathJaxProcessor.replaceMathWrapper(processor)(text));
});
return converter;
};
return Markdown.makeWmdEditor = function(elem, appended_id, imageUploadUrl, postProcessor) {
var $elem, $wmdPanel, $wmdPreviewContainer, ajaxFileUpload, converter, delayRenderer, editor, imageUploadHandler, initialText, wmdInputId, _append;
$elem = $(elem);
if (!$elem.length) {
console.log("warning: elem for makeWmdEditor doesn't exist");
return;
}
if (!$elem.find(".wmd-panel").length) {
initialText = $elem.html();
$elem.empty();
_append = appended_id || "";
wmdInputId = "wmd-input" + _append;
$wmdPreviewContainer = $("<div>").addClass("wmd-preview-container").append($("<div>").addClass("wmd-preview-label").text(gettext("Preview"))).append($("<div>").attr("id", "wmd-preview" + _append).addClass("wmd-panel wmd-preview"));
$wmdPanel = $("<div>").addClass("wmd-panel").append($("<div>").attr("id", "wmd-button-bar" + _append)).append($("<label>").addClass("sr").attr("for", wmdInputId).text(gettext("Post body"))).append($("<textarea>").addClass("wmd-input").attr("id", wmdInputId).html(initialText)).append($wmdPreviewContainer);
$elem.append($wmdPanel);
}
converter = Markdown.getMathCompatibleConverter(postProcessor);
ajaxFileUpload = function(imageUploadUrl, input, startUploadHandler) {
$("#loading").ajaxStart(function() {
return $(this).show();
}).ajaxComplete(function() {
return $(this).hide();
});
$("#upload").ajaxStart(function() {
return $(this).hide();
}).ajaxComplete(function() {
return $(this).show();
});
return $.ajaxFileUpload({
url: imageUploadUrl,
secureuri: false,
fileElementId: 'file-upload',
dataType: 'json',
success: function(data, status) {
var error, fileURL;
fileURL = data['result']['file_url'];
error = data['result']['error'];
if (error !== '') {
alert(error);
if (startUploadHandler) {
$('#file-upload').unbind('change').change(startUploadHandler);
}
return console.log(error);
} else {
return $(input).attr('value', fileURL);
}
},
error: function(data, status, e) {
alert(e);
if (startUploadHandler) {
return $('#file-upload').unbind('change').change(startUploadHandler);
}
}
});
};
imageUploadHandler = function(elem, input) {
return ajaxFileUpload(imageUploadUrl, input, imageUploadHandler);
};
editor = new Markdown.Editor(converter, appended_id, null, imageUploadHandler);
delayRenderer = new MathJaxDelayRenderer();
editor.hooks.chain("onPreviewPush", function(text, previewSet) {
return delayRenderer.render({
text: text,
previewSetter: previewSet
});
});
editor.run();
return editor;
};
}
});
}).call(this);
window.gettext = function(s){return s;};
window.ngettext = function(singular, plural, num){ return num == 1 ? singular : plural }
function interpolate(fmt, obj, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
} else {
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
}
}
// Generated by CoffeeScript 1.6.1
(function() {
var getTime;
getTime = function() {
return new Date().getTime();
};
this.MathJaxDelayRenderer = (function() {
var bufferId, numBuffers;
MathJaxDelayRenderer.prototype.maxDelay = 3000;
MathJaxDelayRenderer.prototype.mathjaxRunning = false;
MathJaxDelayRenderer.prototype.elapsedTime = 0;
MathJaxDelayRenderer.prototype.mathjaxDelay = 0;
MathJaxDelayRenderer.prototype.mathjaxTimeout = void 0;
bufferId = "mathjax_delay_buffer";
numBuffers = 0;
function MathJaxDelayRenderer(params) {
params = params || {};
this.maxDelay = params["maxDelay"] || this.maxDelay;
this.bufferId = params["bufferId"] || (bufferId + numBuffers);
numBuffers += 1;
this.$buffer = $("<div>").attr("id", this.bufferId).css("display", "none").appendTo($("body"));
}
MathJaxDelayRenderer.prototype.render = function(params) {
var delay, elem, preprocessor, previewSetter, renderer, text,
_this = this;
elem = params["element"];
previewSetter = params["previewSetter"];
text = params["text"];
if (text == null) {
text = $(elem).html();
}
preprocessor = params["preprocessor"];
if (params["delay"] === false) {
if (preprocessor != null) {
text = preprocessor(text);
}
$(elem).html(text);
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, $(elem).attr("id")]);
} else {
if (this.mathjaxTimeout) {
window.clearTimeout(this.mathjaxTimeout);
this.mathjaxTimeout = void 0;
}
delay = Math.min(this.elapsedTime + this.mathjaxDelay, this.maxDelay);
renderer = function() {
var curTime, prevTime;
if (_this.mathjaxRunning) {
return;
}
prevTime = getTime();
if (preprocessor != null) {
text = preprocessor(text);
}
_this.$buffer.html(text);
curTime = getTime();
_this.elapsedTime = curTime - prevTime;
if (MathJax) {
prevTime = getTime();
_this.mathjaxRunning = true;
return MathJax.Hub.Queue(["Typeset", MathJax.Hub, _this.$buffer.attr("id")], function() {
_this.mathjaxRunning = false;
curTime = getTime();
_this.mathjaxDelay = curTime - prevTime;
if (previewSetter) {
return previewSetter($(_this.$buffer).html());
} else {
return $(elem).html($(_this.$buffer).html());
}
});
} else {
return _this.mathjaxDelay = 0;
}
};
return this.mathjaxTimeout = window.setTimeout(renderer, delay);
}
};
return MathJaxDelayRenderer;
})();
}).call(this);
/*!
* Cross-Browser Split 1.1.1
* Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
* Available under the MIT License
* ECMAScript compliant, uniform cross-browser split method
*/
/**
* Splits a string into an array of strings using a regex or string separator. Matches of the
* separator are not included in the result array. However, if `separator` is a regex that contains
* capturing groups, backreferences are spliced into the result each time `separator` is matched.
* Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
* cross-browser.
* @param {String} str String to split.
* @param {RegExp|String} separator Regex or string to use for separating the string.
* @param {Number} [limit] Maximum number of items to include in the result array.
* @returns {Array} Array of substrings.
* @example
*
* // Basic use
* split('a b c d', ' ');
* // -> ['a', 'b', 'c', 'd']
*
* // With limit
* split('a b c d', ' ', 2);
* // -> ['a', 'b']
*
* // Backreferences in result array
* split('..word1 word2..', /([a-z]+)(\d+)/i);
* // -> ['..', 'word', '1', ' ', 'word', '2', '..']
*/
var _split; // instead of split for a less common name; avoid conflict
// Avoid running twice; that would break the `nativeSplit` reference
_split = _split || function (undef) {
var nativeSplit = String.prototype.split,
compliantExecNpcg = /()??/.exec("")[1] === undef, // NPCG: nonparticipating capturing group
self;
self = function (str, separator, limit) {
// If `separator` is not a regex, use `nativeSplit`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
return nativeSplit.call(str, separator, limit);
}
var output = [],
flags = (separator.ignoreCase ? "i" : "") +
(separator.multiline ? "m" : "") +
(separator.extended ? "x" : "") + // Proposed for ES6
(separator.sticky ? "y" : ""), // Firefox 3+
lastLastIndex = 0,
// Make `global` and avoid `lastIndex` issues by working with a copy
separator = new RegExp(separator.source, flags + "g"),
separator2, match, lastIndex, lastLength;
str += ""; // Type-convert
if (!compliantExecNpcg) {
// Doesn't need flags gy, but they don't hurt
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
}
/* Values for `limit`, per the spec:
* If undefined: 4294967295 // Math.pow(2, 32) - 1
* If 0, Infinity, or NaN: 0
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
* If other: Type-convert, then use the above rules
*/
limit = limit === undef ?
-1 >>> 0 : // Math.pow(2, 32) - 1
limit >>> 0; // ToUint32(limit)
while (match = separator.exec(str)) {
// `separator.lastIndex` is not reliable cross-browser
lastIndex = match.index + match[0].length;
if (lastIndex > lastLastIndex) {
output.push(str.slice(lastLastIndex, match.index));
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function () {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undef) {
match[i] = undef;
}
}
});
}
if (match.length > 1 && match.index < str.length) {
Array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // Avoid an infinite loop
}
}
if (lastLastIndex === str.length) {
if (lastLength || !separator.test("")) {
output.push("");
}
} else {
output.push(str.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
// For convenience
String.prototype.split = function (separator, limit) {
return self(this, separator, limit);
};
return self;
}();
<%! from django.utils.translation import ugettext as _ %>
<div class="discussion-course">
% if enable_new_post_btn and has_permission_to_create_thread:
<div class="discussion-course-new right">
<a href="#" class="new-post-btn" role="button"><span class="icon icon-edit new-post-icon"></span>${_("New Post")}</a>
</div>
<div style="display: block; clear: both; height: 1px;"></div>
% endif
<section class="discussion container" id="discussion-container"
data-roles="${roles}"
data-course-id="${course_id}"
data-user-info="${user_info}"
data-threads="${threads}"
data-thread-pages="${thread_pages}"
data-content-info="${annotated_content_info}"
data-sort-preference="${sort_preference}"
data-flag-moderator="${flag_moderator}"
data-user-cohort-id="${user_cohort}"
data-course-settings="${course_settings}">
<div class="discussion-body">
<div class="forum-nav"></div>
<div class="discussion-column">
<article class="new-post-article" style="display: none"></article>
<div class="forum-content"></div>
</div>
</div>
</section>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
</div>
<%! from django.utils.translation import ugettext as _ %>
<div class="discussion-module">
<p>
<span class="discussion-preview">
<span class="icon icon-comment"/>
${_("To view live course discussions, click Preview or View Live in Unit Settings.")}
</span>
</p>
</div>
<<<<<<< HEAD:lms/templates/discussion/_underscore_templates.html
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -18,12 +19,14 @@ from django_comment_client.permissions import has_permission ...@@ -18,12 +19,14 @@ from django_comment_client.permissions import has_permission
</div> </div>
<div class="post-extended-content"> <div class="post-extended-content">
<div class="response-count"/> <div class="response-count"/>
% if course is UNDEFINED or has_permission(user, 'create_comment', course.id):
<div class="add-response"> <div class="add-response">
<button class="button add-response-btn"> <button class="button add-response-btn">
<i class="icon fa fa-reply"></i> <i class="icon fa fa-reply"></i>
<span class="add-response-btn-text">${_('Add A Response')}</span> <span class="add-response-btn-text">${_('Add A Response')}</span>
</button> </button>
</div> </div>
% endif
<ol class="responses js-response-list"/> <ol class="responses js-response-list"/>
<div class="response-pagination"/> <div class="response-pagination"/>
<div class="post-status-closed bottom-post-status" style="display: none"> <div class="post-status-closed bottom-post-status" style="display: none">
...@@ -400,7 +403,7 @@ from django_comment_client.permissions import has_permission ...@@ -400,7 +403,7 @@ from django_comment_client.permissions import has_permission
${'<% }); %>'} ${'<% }); %>'}
</select> </select>
</label><div class="field-help" id="field_help_visible_to"> </label><div class="field-help" id="field_help_visible_to">
${_("Discussion admins, moderators, and TAs can make their posts visible to all students or specify a single cohort.")} ${_("Discussion admins, moderators, and TAs can make their posts visible to all students or specify a single cohort group.")}
</div> </div>
</div> </div>
${'<% } %>'} ${'<% } %>'}
......
<%! from django.utils.translation import ugettext as _ %>
<div id="discussion-edit" class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<ul class="list-input settings-list">
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="display-name">Display Name</label>
<input class="input setting-input" id="display-name" value="${display_name}" type="text">
</div>
<span class="tip setting-help">Display Name</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="discussion-category">Discussion Category</label>
<input class="input setting-input" id="discussion-category" value="${discussion_category}" type="text">
</div>
<span class="tip setting-help">Discussion Category</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="discussion-target">Discussion Target</label>
<input class="input setting-input" id="discussion-target" value="${discussion_target}" type="text">
</div>
<span class="tip setting-help">Discussion Target</span>
</li>
</ul>
<div class="xblock-actions">
<span class="xblock-editor-error-message"></span>
<ul>
<li class="action-item">
<a href="#" class="button action-primary save-button">${_('Save')}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">${_('Cancel')}</a>
</li>
</ul>
</div>
</div>
<%! from django.utils.translation import ugettext as _ %>
<%! import django_comment_client.helpers as helpers %>
<%! from django.template.defaultfilters import escapejs %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
import django_comment_client.helpers as helpers
from django.template.defaultfilters import escapejs
from django.core.urlresolvers import reverse
%>
<%block name="bodyclass">discussion</%block> <%block name="bodyclass">discussion</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</%block> <%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default) | h}</%block>
<%block name="nav_skip">#discussion-container</%block> <%block name="nav_skip">#discussion-container</%block>
...@@ -14,10 +12,12 @@ from django.core.urlresolvers import reverse ...@@ -14,10 +12,12 @@ from django.core.urlresolvers import reverse
<%block name="headextra"> <%block name="headextra">
<%static:css group='style-course-vendor'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" />
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
<%include file="/discussion/_js_body_dependencies.html" /> <%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/> <%static:js group='discussion'/>
</%block> </%block>
...@@ -35,9 +35,9 @@ from django.core.urlresolvers import reverse ...@@ -35,9 +35,9 @@ from django.core.urlresolvers import reverse
data-user-cohort-id="${user_cohort}" data-user-cohort-id="${user_cohort}"
data-course-settings="${course_settings}"> data-course-settings="${course_settings}">
<div class="discussion-body"> <div class="discussion-body">
<div class="forum-nav" role="complementary" aria-label="${_("Discussion thread list")}"></div> <div class="forum-nav"></div>
<div class="discussion-column" role="main" aria-label="Discussion" id="discussion-column"> <div class="discussion-column">
<article class="new-post-article" style="display: none" tabindex="-1" aria-label="${_("New topic form")}"></article> <article class="new-post-article" style="display: none"></article>
<div class="forum-content"></div> <div class="forum-content"></div>
</div> </div>
</div> </div>
...@@ -45,5 +45,3 @@ from django.core.urlresolvers import reverse ...@@ -45,5 +45,3 @@ from django.core.urlresolvers import reverse
<%include file="_underscore_templates.html" /> <%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" /> <%include file="_thread_list_template.html" />
<%include file="/discussion/_discussion_course.html" />
...@@ -11,10 +11,12 @@ from django.template.defaultfilters import escapejs ...@@ -11,10 +11,12 @@ from django.template.defaultfilters import escapejs
<%block name="headextra"> <%block name="headextra">
<%static:css group='style-course-vendor'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" />
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
<%include file="/discussion/_js_body_dependencies.html" /> <%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/> <%static:js group='discussion'/>
</%block> </%block>
...@@ -26,7 +28,7 @@ from django.template.defaultfilters import escapejs ...@@ -26,7 +28,7 @@ from django.template.defaultfilters import escapejs
<nav aria-label="${_('User Profile')}"> <nav aria-label="${_('User Profile')}">
<article class="sidebar-module discussion-sidebar"> <article class="sidebar-module discussion-sidebar">
<%include file="/discussion/_user_profile.html" /> <%include file="_user_profile.html" />
</article> </article>
</nav> </nav>
...@@ -36,4 +38,4 @@ from django.template.defaultfilters import escapejs ...@@ -36,4 +38,4 @@ from django.template.defaultfilters import escapejs
</div> </div>
</section> </section>
<%include file="/discussion/_underscore_templates.html" /> <%include file="_underscore_templates.html" />
<div class="discussion-post">
<header>
<div class="group-visibility-label">
<% if (obj.group_name) { %>
<%-
interpolate(
gettext('This post is visible only to %(group_name)s.'),
{group_name: obj.group_name},
true
)
%>
<% } else { %>
<%- gettext('This post is visible to everyone.') %>
<% } %>
</div>
<div class="post-header-content">
<h1><%- title %></h1>
<p class="posted-details">
<%
var timeAgoHtml = interpolate(
'<span class="timeago" title="%(created_at)s">%(created_at)s</span>',
{created_at: created_at},
true
);
%>
<%=
interpolate(
// Translators: post_type describes the kind of post this is (e.g. "question" or "discussion");
// time_ago is how much time has passed since the post was created (e.g. "4 hours ago")
_.escape(gettext('%(post_type)s posted %(time_ago)s by %(author)s')),
{post_type: thread_type, time_ago: timeAgoHtml, author: author_display},
true
)
%>
</p>
<div class="post-labels">
<span class="post-label-pinned"><i class="icon icon-pushpin"></i><%- gettext("Pinned") %></span>
<span class="post-label-reported"><i class="icon icon-flag"></i><%- gettext("Reported") %></span>
<span class="post-label-closed"><i class="icon icon-lock"></i><%- gettext("Closed") %></span>
</div>
</div>
<div class="post-header-actions post-extended-content">
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close']
}
)
%>
</div>
</header>
<div class="post-body"><%- body %></div>
<% if (mode == "tab" && obj.courseware_url) { %>
<%
var courseware_title_linked = interpolate(
'<a href="%(courseware_url)s">%(courseware_title)s</a>',
{courseware_url: courseware_url, courseware_title: _.escape(courseware_title)},
true
);
%>
<div class="post-context">
<%=
interpolate(
_.escape(gettext('(this post is about %(courseware_title_linked)s)')),
{courseware_title_linked: courseware_title_linked},
true
)
%>
</div>
<% } %>
</div>
...@@ -381,13 +381,28 @@ class DiscussionTabSingleThreadPage(CoursePage): ...@@ -381,13 +381,28 @@ class DiscussionTabSingleThreadPage(CoursePage):
class InlineDiscussionPage(PageObject): class InlineDiscussionPage(PageObject):
url = None url = None
def __init__(self, browser, discussion_id): def __init__(self, browser, discussion_id=None):
super(InlineDiscussionPage, self).__init__(browser) super(InlineDiscussionPage, self).__init__(browser)
self._discussion_selector = ( if not discussion_id is None:
"body.courseware .discussion-module[data-discussion-id='{discussion_id}'] ".format( self._discussion_selector = (
discussion_id=discussion_id "body.courseware .discussion-module[data-discussion-id='{discussion_id}'] ".format(
discussion_id=discussion_id
)
) )
) self._discussion_id = discussion_id
else:
self._discussion_selector = "body.courseware .discussion-module[data-discussion-id] "
self._discussion_id = None
def get_discussion_id(self):
"""
Gets configured discussion_id for the page.
Uses discussion_id passed to __init__ if it was not None, otherwise picks it from XBlock element data attribute
"""
if self._discussion_id is not None:
return self._discussion_id
return self.q(css=self._discussion_selector).first.attrs('data-discussion-id')[0]
def _find_within(self, selector): def _find_within(self, selector):
""" """
......
...@@ -3,11 +3,9 @@ import os ...@@ -3,11 +3,9 @@ import os
from django.conf import settings from django.conf import settings
from mako.template import Template from mako.template import Template
from discussion_app.views import get_template_dir as discussion_get_template_dir
def include_mustache_templates(): def include_mustache_templates():
mustache_dir = discussion_get_template_dir() + '/discussion/mustache' mustache_dir = settings.COMMON_ROOT / 'templates' / 'discussion' / 'mustache'
def is_valid_file_name(file_name): def is_valid_file_name(file_name):
return file_name.endswith('.mustache') return file_name.endswith('.mustache')
......
...@@ -43,11 +43,6 @@ from xmodule.modulestore.modulestore_settings import update_module_store_setting ...@@ -43,11 +43,6 @@ from xmodule.modulestore.modulestore_settings import update_module_store_setting
from xmodule.mixin import LicenseMixin from xmodule.mixin import LicenseMixin
from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin
from discussion_app.views import (
get_js_urls as discussion_get_js_urls,
get_css_urls as discussion_get_css_urls
)
################################### FEATURES ################################### ################################### FEATURES ###################################
# The display name of the platform to be used in templates/emails/etc. # The display name of the platform to be used in templates/emails/etc.
PLATFORM_NAME = "Your Platform Name Here" PLATFORM_NAME = "Your Platform Name Here"
...@@ -1262,14 +1257,14 @@ main_vendor_js = base_vendor_js + [ ...@@ -1262,14 +1257,14 @@ main_vendor_js = base_vendor_js + [
'js/vendor/swfobject/swfobject.js', 'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.js', 'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/URI.min.js', 'js/vendor/URI.min.js',
'js/vendor/underscore-min.js',
'js/vendor/backbone-min.js',
] ]
dashboard_js = (
sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js'))
)
dashboard_search_js = ['js/search/dashboard/main.js'] dashboard_search_js = ['js/search/dashboard/main.js']
rwd_header_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/utils/rwd_header.js')) rwd_header_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/utils/rwd_header.js'))
discussion_js = discussion_get_js_urls() dashboard_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js'))
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js')) open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js'))
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js')) notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
...@@ -1411,10 +1406,6 @@ PIPELINE_CSS = { ...@@ -1411,10 +1406,6 @@ PIPELINE_CSS = {
], ],
'output_filename': 'css/lms-main-rtl.css', 'output_filename': 'css/lms-main-rtl.css',
}, },
'style-discussion-app': {
'source_filenames': discussion_get_css_urls(),
'output_filename': 'css/lms-style-discussion-app.css',
},
'style-course-vendor': { 'style-course-vendor': {
'source_filenames': [ 'source_filenames': [
'js/vendor/CodeMirror/codemirror.css', 'js/vendor/CodeMirror/codemirror.css',
...@@ -1865,7 +1856,6 @@ INSTALLED_APPS = ( ...@@ -1865,7 +1856,6 @@ INSTALLED_APPS = (
'django_comment_common', 'django_comment_common',
'discussion_api', 'discussion_api',
'notes', 'notes',
'discussion_app',
'edxnotes', 'edxnotes',
......
// forums - main app styling // forums - main app styling
// ==================== // ====================
body.discussion, .discussion-course { body.discussion, .discussion-course, div.discussion-body {
// new post creation // new post creation
.new-post-form-errors { .new-post-form-errors {
......
// discussion - elements - labels // discussion - elements - labels
// ==================== // ====================
body.discussion, .discussion-module { body.discussion, .discussion-module, .discussion-body {
.post-label-pinned { .post-label-pinned {
@include forum-post-label($forum-color-pinned); @include forum-post-label($forum-color-pinned);
} }
......
<%! from django.utils.translation import ugettext as _ %>
<div class="blank-state">
% if performed_search:
${_("Sorry! We can't find anything matching your search. Please try another search.")}
% else:
${_("There are no posts here yet. Be the first one to post!")}
% endif
</div>
<%! from django.template.defaultfilters import escapejs %>
<script type="text/javascript">
var $$course_id = "${course_id | escapejs}";
if (typeof $$annotated_content_info === undefined) {
var $$annotated_content_info = {};
}
$$annotated_content_info = $.extend($$annotated_content_info, JSON.parse("${annotated_content_info | escapejs}"));
if (typeof $$discussion_data === undefined) {
var $$discussion_data = {};
}
$$discussion_data = $.extend($$discussion_data, JSON.parse("${discussion_data | escapejs}"));
</script>
<%!
from django.utils.translation import ugettext as _
import django_comment_client.helpers as helpers
%>
% if recent_active_threads:
<article class="discussion-sidebar-following sidebar-module">
<header>
<h4>${_("Following")}</h4>
<!--<a href="#" class="sidebar-view-all">view all &rsaquo;</a>-->
</header>
<ol class="discussion-sidebar-following-list">
% for thread in recent_active_threads:
<li><a href="${helpers.permalink(thread) | h}"><span class="sidebar-following-name">${thread['title'] | h}</span> <span class="sidebar-vote-count">${thread['votes']['point'] | h}</span></a></li>
% endfor
<ol>
</article>
% endif
<%!
from django.utils.translation import ugettext as _
from urllib import urlencode
def merge(dic1, dic2):
return dict(dic1.items() + dic2.items())
def base_url_for_search():
return base_url + '?' + urlencode(merge(query_params, {'page': 1}))
%>
<form action="${base_url_for_search()}" method="get" class="discussion-search-form">
<input class="search-input" type="text" value="${text | h}" id="keywords" autocomplete="off"/>
<div class="discussion-link discussion-search-link" href="javascript:void(0)">${_("Search posts")}</div>
</form>
...@@ -66,7 +66,6 @@ from branding import api as branding_api ...@@ -66,7 +66,6 @@ from branding import api as branding_api
<%static:css group='style-app'/> <%static:css group='style-app'/>
<%static:css group='style-app-extend1'/> <%static:css group='style-app-extend1'/>
<%static:css group='style-app-extend2'/> <%static:css group='style-app-extend2'/>
<%static:css group='style-discussion-app'/>
% if disable_courseware_js: % if disable_courseware_js:
<%static:js group='base_vendor'/> <%static:js group='base_vendor'/>
......
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
{% compressed_css 'style-main' %} {% compressed_css 'style-main' %}
{% compressed_css 'style-course-vendor' %} {% compressed_css 'style-course-vendor' %}
{% compressed_css 'style-course' %} {% compressed_css 'style-course' %}
{% compressed_css 'style-discussion-app' %}
{% block main_vendor_js %} {% block main_vendor_js %}
{% compressed_js 'main_vendor' %} {% compressed_js 'main_vendor' %}
......
...@@ -8,3 +8,6 @@ ...@@ -8,3 +8,6 @@
-e common/lib/sandbox-packages -e common/lib/sandbox-packages
-e common/lib/symmath -e common/lib/symmath
-e common/lib/xmodule -e common/lib/xmodule
# XBlocks
-e common/lib/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