Commit 0f553c13 by Xavier Antoviaque

Merge pull request #323 from open-craft/eugeny/discussion-backport

Production deployment fixes
parents 241c7069 d9f7b6f3
...@@ -389,6 +389,10 @@ PIPELINE_CSS = { ...@@ -389,6 +389,10 @@ PIPELINE_CSS = {
], ],
'output_filename': 'css/cms-style-xmodule-annotations.css', 'output_filename': 'css/cms-style-xmodule-annotations.css',
}, },
'discussion': {
'source_filenames': ['sass/discussion-forum.css'],
'output_filename': 'css/cms-style-discussion-forum.css'
}
} }
# test_order: Determines the position of this chunk of javascript on # test_order: Determines the position of this chunk of javascript on
......
var $$course_id = "{{course_id}}"; var $$course_id = "{{course_id}}";
function DiscussionCourseBlock(runtime, element) { function DiscussionCourseBlock(runtime, element) {
var el = $(element).find('section.discussion'), var el = $(element).find('section.discussion');
pushState = true;
var testUrl = runtime.handlerUrl(element, 'test'); var testUrl = runtime.handlerUrl(element, 'test');
if (testUrl.match(/^(http|https):\/\//)) { if (testUrl.match(/^(http|https):\/\//)) {
var hostname = testUrl.match(/^(.*:\/\/[a-z\-.]+)\//)[1]; var hostname = testUrl.match(/^(.*:\/\/[a-z0-9:\-.]+)\//)[1];
DiscussionUtil.setBaseUrl(hostname); DiscussionUtil.setBaseUrl(hostname);
DiscussionUtil.localUrls.push('user_profile');
DiscussionUtil.force_async = true;
pushState = false
} }
DiscussionApp.start(el, pushState); if (runtime.local_overrides && runtime.local_overrides.discussion) {
runtime.local_overrides.discussion(element, DiscussionUtil);
}
DiscussionApp.start(el);
} }
...@@ -7,7 +7,10 @@ function DiscussionInlineBlock(runtime, element) { ...@@ -7,7 +7,10 @@ function DiscussionInlineBlock(runtime, element) {
if (testUrl.match(/^(http|https):\/\//)) { if (testUrl.match(/^(http|https):\/\//)) {
var hostname = testUrl.match(/^(.*:\/\/[a-z0-9:\-.]+)\//)[1]; var hostname = testUrl.match(/^(.*:\/\/[a-z0-9:\-.]+)\//)[1];
DiscussionUtil.setBaseUrl(hostname); DiscussionUtil.setBaseUrl(hostname);
DiscussionUtil.force_async = true; }
if (runtime.local_overrides && runtime.local_overrides.discussion) {
runtime.local_overrides.discussion(element, DiscussionUtil);
} }
new DiscussionModuleView({ el: el }); new DiscussionModuleView({ el: el });
......
...@@ -17,7 +17,6 @@ JS_URLS = [ ...@@ -17,7 +17,6 @@ JS_URLS = [
'js/vendor/jquery.leanModal.min.js', 'js/vendor/jquery.leanModal.min.js',
'js/vendor/jquery.timeago.js', 'js/vendor/jquery.timeago.js',
'js/vendor/underscore-min.js', 'js/vendor/underscore-min.js',
'js/vendor/backbone-min.js',
'js/vendor/mustache.js', 'js/vendor/mustache.js',
'js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full', 'js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full',
...@@ -31,16 +30,9 @@ JS_URLS = [ ...@@ -31,16 +30,9 @@ JS_URLS = [
] ]
CSS_URLS = [ CSS_URLS = [
'xblock/discussion/css/vendor/font-awesome.css', 'xblock/discussion/css/vendor/font-awesome.css'
'sass/discussion-forum.css',
] ]
main_js = u'coffee/src/discussion/main.js'
all_js = set(rooted_glob(settings.COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
all_js.remove(main_js)
discussion_js = sorted(all_js) + [main_js]
def load_resource(resource_path): def load_resource(resource_path):
""" """
...@@ -128,22 +120,12 @@ def load_scenarios_from_path(scenarios_path): ...@@ -128,22 +120,12 @@ def load_scenarios_from_path(scenarios_path):
return get_scenarios_from_path(scenarios_path, include_identifier=True) 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 + discussion_js]
def get_css_urls():
""" Returns a list of all additional css files """
return [asset_to_static_url(path) for path in CSS_URLS]
def add_resources_to_fragment(fragment): def add_resources_to_fragment(fragment):
# order is important : code in discussion_thread_list_view -> updateSidebar depends on the # order is important : code in discussion_thread_list_view -> updateSidebar depends on the
# fact that html is properly styled as it does height calculations. # fact that html is properly styled as it does height calculations.
for url in CSS_URLS: for url in CSS_URLS:
fragment.add_css_url(asset_to_static_url(url)) fragment.add_css_url(asset_to_static_url(url))
for url in JS_URLS + discussion_js: for url in JS_URLS:
fragment.add_javascript_url(asset_to_static_url(url)) fragment.add_javascript_url(asset_to_static_url(url))
......
if Backbone? if Backbone?
class @DiscussionRouter extends Backbone.Router class @DiscussionRouter extends Backbone.Router
routes:
"": "allThreads"
":forum_name/threads/:thread_id" : "showThread"
initialize: (options) -> initialize: (options) ->
@route("#{DiscussionUtil.route_prefix}", "allThreads")
@route("#{DiscussionUtil.route_prefix}:forum_name/threads/:thread_id", "showThread")
@discussion = options['discussion'] @discussion = options['discussion']
@course_settings = options['course_settings'] @course_settings = options['course_settings']
...@@ -14,7 +13,7 @@ if Backbone? ...@@ -14,7 +13,7 @@ if Backbone?
courseSettings: @course_settings courseSettings: @course_settings
) )
@nav.on "thread:selected", @navigateToThread @nav.on "thread:selected", @navigateToThread
@nav.on "thread:removed", @navigateToAllThreads @nav.on "thread:deselected", @navigateToAllThreads
@nav.on "threads:rendered", @setActiveThread @nav.on "threads:rendered", @setActiveThread
@nav.on "thread:created", @navigateToThread @nav.on "thread:created", @navigateToThread
@nav.render() @nav.render()
...@@ -59,6 +58,7 @@ if Backbone? ...@@ -59,6 +58,7 @@ if Backbone?
@main = new DiscussionThreadView( @main = new DiscussionThreadView(
el: $(".forum-content"), el: $(".forum-content"),
model: @thread, model: @thread,
mode: "tab", mode: "tab",
course_settings: @course_settings, course_settings: @course_settings,
...@@ -70,10 +70,10 @@ if Backbone? ...@@ -70,10 +70,10 @@ if Backbone?
navigateToThread: (thread_id) => navigateToThread: (thread_id) =>
thread = @discussion.get(thread_id) thread = @discussion.get(thread_id)
@navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true) @navigate("#{DiscussionUtil.route_prefix}#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true)
navigateToAllThreads: => navigateToAllThreads: =>
@navigate("", trigger: true) @navigate("#{DiscussionUtil.route_prefix}", trigger: true)
showNewPost: (event) => showNewPost: (event) =>
$('.forum-content').fadeOut( $('.forum-content').fadeOut(
......
if Backbone? if Backbone?
@DiscussionApp = @DiscussionApp =
start: (elem, pushState = true)-> start: (elem)->
# 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,10 @@ if Backbone? ...@@ -17,7 +17,10 @@ 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: pushState, root: "/courses/#{$$course_id}/discussion/forum/"}) if !Backbone.History.started
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
else
Backbone.history.loadUrl(window.location.pathname)
@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
......
...@@ -19,6 +19,7 @@ class @DiscussionUtil ...@@ -19,6 +19,7 @@ class @DiscussionUtil
@localUrls: [] @localUrls: []
@force_async = false @force_async = false
@route_prefix = ''
@getTemplate: (id) -> @getTemplate: (id) ->
$("script##{id}").html() $("script##{id}").html()
......
...@@ -15,6 +15,7 @@ if Backbone? ...@@ -15,6 +15,7 @@ if Backbone?
"change .forum-nav-filter-cohort-control": "chooseCohort" "change .forum-nav-filter-cohort-control": "chooseCohort"
initialize: (options) -> initialize: (options) ->
@active_thread_id = null
@courseSettings = options.courseSettings @courseSettings = options.courseSettings
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages) @displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
@collection.on "change", @reloadDisplayedCollection @collection.on "change", @reloadDisplayedCollection
...@@ -29,8 +30,9 @@ if Backbone? ...@@ -29,8 +30,9 @@ if Backbone?
# if target.length > 0 # if target.length > 0
# @filterTopic($.Event("filter", {'target': target[0]})) # @filterTopic($.Event("filter", {'target': target[0]}))
@collection.on "add", @addAndSelectThread @collection.on "add", @addAndSelectThread
@collection.on "thread:remove", @threadRemoved
@sidebar_padding = 10 @sidebar_padding = 10
@boardName @boardName = null
@template = _.template($("#thread-list-template").html()) @template = _.template($("#thread-list-template").html())
@current_search = "" @current_search = ""
@mode = 'all' @mode = 'all'
...@@ -239,10 +241,12 @@ if Backbone? ...@@ -239,10 +241,12 @@ if Backbone?
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above... @trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
false false
threadRemoved: (thread_id) => threadRemoved: (thread) =>
@trigger("thread:removed", thread_id) if @active_thread_id == thread.id
@trigger("thread:deselected", thread.id)
setActiveThread: (thread_id) -> setActiveThread: (thread_id) ->
@active_thread_id = thread_id
@$(".forum-nav-thread[data-id!='#{thread_id}'] .forum-nav-thread-link").removeClass("is-active") @$(".forum-nav-thread[data-id!='#{thread_id}'] .forum-nav-thread-link").removeClass("is-active")
@$(".forum-nav-thread[data-id='#{thread_id}'] .forum-nav-thread-link").addClass("is-active") @$(".forum-nav-thread[data-id='#{thread_id}'] .forum-nav-thread-link").addClass("is-active")
...@@ -253,15 +257,13 @@ if Backbone? ...@@ -253,15 +257,13 @@ if Backbone?
$("input.email-setting").bind "click", @updateEmailNotifications $("input.email-setting").bind "click", @updateEmailNotifications
url = DiscussionUtil.urlFor("notifications_status",window.user.get("id")) url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
url: url url: url
type: "GET" type: "GET"
success: (response, textStatus) => success: (response, textStatus) =>
if response.status if response.status
$('input.email-setting').attr('checked','checked') $('input.email-setting').attr('checked','checked')
else else
$('input.email-setting').removeAttr('checked') $('input.email-setting').removeAttr('checked')
thread_id = null
@trigger("thread:removed")
#select all threads #select all threads
isBrowseMenuVisible: => isBrowseMenuVisible: =>
......
...@@ -34,6 +34,8 @@ if Backbone? ...@@ -34,6 +34,8 @@ if Backbone?
if @isQuestion() if @isQuestion()
@markedAnswers = new Comments() @markedAnswers = new Comments()
@options = options
rerender: () -> rerender: () ->
if @showView? if @showView?
@showView.undelegateEvents() @showView.undelegateEvents()
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<%block name="js_extra">
<%static:js group='discussion'/>
</%block>
<%block name="css_extra">
<%static:css group='discussion'/>
</%block>
<div class="discussion-course"> <div class="discussion-course">
% if enable_new_post_btn and has_permission_to_create_thread: % if enable_new_post_btn and has_permission_to_create_thread:
......
...@@ -2,6 +2,16 @@ ...@@ -2,6 +2,16 @@
<%! from django_comment_client.permissions import has_permission %> <%! from django_comment_client.permissions import has_permission %>
<%include file="_underscore_templates.html" /> <%include file="_underscore_templates.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="js_extra">
<%static:js group='discussion'/>
</%block>
<%block name="css_extra">
<%static:css group='discussion'/>
</%block>
<div class="discussion-module" data-discussion-id="${discussion_id | h}"> <div class="discussion-module" data-discussion-id="${discussion_id | h}">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}" role="button"><span class="show-hide-discussion-icon"></span><span class="button-text">${_("Show Discussion")}</span></a> <a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}" role="button"><span class="show-hide-discussion-icon"></span><span class="button-text">${_("Show Discussion")}</span></a>
% if has_permission(user, 'create_thread', course.id): % if has_permission(user, 'create_thread', course.id):
......
"""
Helper functions and classes for discussion tests.
"""
from uuid import uuid4
from ...fixtures.discussion import (
SingleThreadViewFixture,
Thread,
Response,
)
class BaseDiscussionMixin(object):
"""
A mixin containing methods common to discussion tests.
"""
def setup_thread(self, num_responses, **thread_kwargs):
"""
Create a test thread with the given number of responses, passing all
keyword arguments through to the Thread fixture, then invoke
setup_thread_page.
"""
thread_id = "test_thread_{}".format(uuid4().hex)
thread_fixture = SingleThreadViewFixture(
Thread(id=thread_id, commentable_id=self.discussion_id, **thread_kwargs)
)
for i in range(num_responses):
thread_fixture.addResponse(Response(id=str(i), body=str(i)))
thread_fixture.push()
self.setup_thread_page(thread_id)
return thread_id
...@@ -7,10 +7,10 @@ from pytz import UTC ...@@ -7,10 +7,10 @@ from pytz import UTC
from uuid import uuid4 from uuid import uuid4
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from .helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ..pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
from ..pages.lms.courseware import CoursewarePage from ...pages.lms.courseware import CoursewarePage
from ..pages.lms.discussion import ( from ...pages.lms.discussion import (
DiscussionTabSingleThreadPage, DiscussionTabSingleThreadPage,
InlineDiscussionPage, InlineDiscussionPage,
InlineDiscussionThreadPage, InlineDiscussionThreadPage,
...@@ -18,8 +18,8 @@ from ..pages.lms.discussion import ( ...@@ -18,8 +18,8 @@ from ..pages.lms.discussion import (
DiscussionTabHomePage, DiscussionTabHomePage,
DiscussionSortPreferencePage, DiscussionSortPreferencePage,
) )
from ..fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ..fixtures.discussion import ( from ...fixtures.discussion import (
SingleThreadViewFixture, SingleThreadViewFixture,
UserProfileViewFixture, UserProfileViewFixture,
SearchResultFixture, SearchResultFixture,
...@@ -29,28 +29,14 @@ from ..fixtures.discussion import ( ...@@ -29,28 +29,14 @@ from ..fixtures.discussion import (
SearchResult, SearchResult,
) )
from .helpers import BaseDiscussionMixin
class DiscussionResponsePaginationTestMixin(object):
class DiscussionResponsePaginationTestMixin(BaseDiscussionMixin):
""" """
A mixin containing tests for response pagination for use by both inline A mixin containing tests for response pagination for use by both inline
discussion and the discussion tab discussion and the discussion tab
""" """
def setup_thread(self, num_responses, **thread_kwargs):
"""
Create a test thread with the given number of responses, passing all
keyword arguments through to the Thread fixture, then invoke
setup_thread_page.
"""
thread_id = "test_thread_{}".format(uuid4().hex)
thread_fixture = SingleThreadViewFixture(
Thread(id=thread_id, commentable_id=self.discussion_id, **thread_kwargs)
)
for i in range(num_responses):
thread_fixture.addResponse(Response(id=str(i), body=str(i)))
thread_fixture.push()
self.setup_thread_page(thread_id)
def assert_response_display_correct(self, response_total, displayed_responses): def assert_response_display_correct(self, response_total, displayed_responses):
""" """
Assert that various aspects of the display of responses are all correct: Assert that various aspects of the display of responses are all correct:
...@@ -300,24 +286,34 @@ class DiscussionCommentEditTest(UniqueCourseTest): ...@@ -300,24 +286,34 @@ class DiscussionCommentEditTest(UniqueCourseTest):
self.assertTrue(page.is_add_comment_visible("response1")) self.assertTrue(page.is_add_comment_visible("response1"))
@attr('shard_1') class InlineDiscussionTestMixin(BaseDiscussionMixin):
class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMixin):
""" """
Tests for inline discussions Tests for inline discussions
""" """
def _get_xblock_fixture_desc(self):
""" Returns Discussion XBlockFixtureDescriptor """
raise NotImplementedError()
def _initial_discussion_id(self):
""" Returns initial discussion_id for InlineDiscussionPage """
raise NotImplementedError()
@property
def discussion_id(self):
""" Returns selected discussion_id """
raise NotImplementedError()
def __init__(self, *args, **kwargs):
self._discussion_id = None
super(InlineDiscussionTestMixin, self).__init__(*args, **kwargs)
def setUp(self): def setUp(self):
super(InlineDiscussionTest, self).setUp() super(InlineDiscussionTestMixin, self).setUp()
self.discussion_id = "test_discussion_{}".format(uuid4().hex)
self.course_fix = CourseFixture(**self.course_info).add_children( self.course_fix = CourseFixture(**self.course_info).add_children(
XBlockFixtureDesc("chapter", "Test Section").add_children( XBlockFixtureDesc("chapter", "Test Section").add_children(
XBlockFixtureDesc("sequential", "Test Subsection").add_children( XBlockFixtureDesc("sequential", "Test Subsection").add_children(
XBlockFixtureDesc("vertical", "Test Unit").add_children( XBlockFixtureDesc("vertical", "Test Unit").add_children(
XBlockFixtureDesc( self._get_xblock_fixture_desc()
"discussion",
"Test Discussion",
metadata={"discussion_id": self.discussion_id}
)
) )
) )
) )
...@@ -327,7 +323,7 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix ...@@ -327,7 +323,7 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.courseware_page.visit() self.courseware_page.visit()
self.discussion_page = InlineDiscussionPage(self.browser, self.discussion_id) self.discussion_page = InlineDiscussionPage(self.browser, self._initial_discussion_id())
def setup_thread_page(self, thread_id): def setup_thread_page(self, thread_id):
self.discussion_page.expand_discussion() self.discussion_page.expand_discussion()
...@@ -391,6 +387,49 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix ...@@ -391,6 +387,49 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix
@attr('shard_1') @attr('shard_1')
class DiscussionXModuleInlineTest(InlineDiscussionTestMixin, UniqueCourseTest, DiscussionResponsePaginationTestMixin):
""" Discussion XModule inline mode tests """
def _get_xblock_fixture_desc(self):
""" Returns Discussion XBlockFixtureDescriptor """
return XBlockFixtureDesc(
'discussion',
"Test Discussion",
metadata={"discussion_id": self.discussion_id}
)
def _initial_discussion_id(self):
""" Returns initial discussion_id for InlineDiscussionPage """
return self.discussion_id
@property
def discussion_id(self):
""" Returns selected discussion_id """
if getattr(self, '_discussion_id', None) is None:
self._discussion_id = "test_discussion_{}".format(uuid4().hex)
return self._discussion_id
@attr('shard_1')
class DiscussionXBlockInlineTest(InlineDiscussionTestMixin, UniqueCourseTest, DiscussionResponsePaginationTestMixin):
""" Discussion XBlock inline mode tests """
def _get_xblock_fixture_desc(self):
""" Returns Discussion XBlockFixtureDescriptor """
return XBlockFixtureDesc(
'discussion-forum',
"Test Discussion"
)
def _initial_discussion_id(self):
""" Returns initial discussion_id for InlineDiscussionPage """
return None
@property
def discussion_id(self):
""" Returns selected discussion_id """
return self.discussion_page.get_discussion_id()
@attr('shard_1')
class DiscussionUserProfileTest(UniqueCourseTest): class DiscussionUserProfileTest(UniqueCourseTest):
""" """
Tests for user profile page in discussion tab. Tests for user profile page in discussion tab.
......
...@@ -26,7 +26,8 @@ from django_comment_client.utils import ( ...@@ -26,7 +26,8 @@ from django_comment_client.utils import (
get_ability, get_ability,
JsonError, JsonError,
JsonResponse, JsonResponse,
safe_content safe_content,
get_discussion_categories_ids
) )
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
import lms.lib.comment_client as cc import lms.lib.comment_client as cc
...@@ -164,6 +165,18 @@ def update_thread(request, course_id, thread_id): ...@@ -164,6 +165,18 @@ def update_thread(request, course_id, thread_id):
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
thread.body = request.POST["body"] thread.body = request.POST["body"]
thread.title = request.POST["title"] thread.title = request.POST["title"]
# The following checks should avoid issues we've seen during deploys, where end users are hitting an updated server
# while their browser still has the old client code. This will avoid erasing present values in those cases.
if "thread_type" in request.POST:
thread.thread_type = request.POST["thread_type"]
if "commentable_id" in request.POST:
course = get_course_with_access(request.user, 'load', course_key)
commentable_ids = get_discussion_categories_ids(course)
if request.POST.get("commentable_id") in commentable_ids:
thread.commentable_id = request.POST["commentable_id"]
else:
return JsonError(_("Topic doesn't exist"))
thread.save() thread.save()
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_key, thread.to_dict()) return ajax_content_response(request, course_key, thread.to_dict())
......
...@@ -42,7 +42,7 @@ class DictionaryTestCase(TestCase): ...@@ -42,7 +42,7 @@ class DictionaryTestCase(TestCase):
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class AccessUtilsTestCase(TestCase): class AccessUtilsTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.course_id = self.course.id self.course_id = self.course.id
...@@ -148,7 +148,7 @@ class CategoryMapTestCase(ModuleStoreTestCase): ...@@ -148,7 +148,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
self.course.discussion_topics = {} self.course.discussion_topics = {}
self.course.save() self.course.save()
self.discussion_num = 0 self.discussion_num = 0
self.maxDiff = None # pylint: disable=C0103 self.maxDiff = None # pylint: disable=C0103
def create_discussion(self, discussion_category, discussion_target, **kwargs): def create_discussion(self, discussion_category, discussion_target, **kwargs):
self.discussion_num += 1 self.discussion_num += 1
...@@ -193,7 +193,7 @@ class CategoryMapTestCase(ModuleStoreTestCase): ...@@ -193,7 +193,7 @@ class CategoryMapTestCase(ModuleStoreTestCase):
} }
) )
check_cohorted_topics([]) # default (empty) cohort config check_cohorted_topics([]) # default (empty) cohort config
self.course.cohort_config = {"cohorted": False, "cohorted_discussions": []} self.course.cohort_config = {"cohorted": False, "cohorted_discussions": []}
check_cohorted_topics([]) check_cohorted_topics([])
...@@ -564,6 +564,46 @@ class CategoryMapTestCase(ModuleStoreTestCase): ...@@ -564,6 +564,46 @@ class CategoryMapTestCase(ModuleStoreTestCase):
} }
) )
def test_ids_empty(self):
self.assertEqual(utils.get_discussion_categories_ids(self.course), [])
def test_ids_configured_topics(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A"},
"Topic B": {"id": "Topic_B"},
"Topic C": {"id": "Topic_C"}
}
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
["Topic_A", "Topic_B", "Topic_C"]
)
def test_ids_inline(self):
self.create_discussion("Chapter 1", "Discussion 1")
self.create_discussion("Chapter 1", "Discussion 2")
self.create_discussion("Chapter 2", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion")
self.create_discussion("Chapter 3 / Section 1", "Discussion")
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
["discussion1", "discussion2", "discussion3", "discussion4", "discussion5", "discussion6"]
)
def test_ids_mixed(self):
self.course.discussion_topics = {
"Topic A": {"id": "Topic_A"},
"Topic B": {"id": "Topic_B"},
"Topic C": {"id": "Topic_C"}
}
self.create_discussion("Chapter 1", "Discussion 1")
self.create_discussion("Chapter 2", "Discussion")
self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion")
self.assertItemsEqual(
utils.get_discussion_categories_ids(self.course),
["Topic_A", "Topic_B", "Topic_C", "discussion1", "discussion2", "discussion3"]
)
class JsonResponseTestCase(TestCase, UnicodeTestMixin): class JsonResponseTestCase(TestCase, UnicodeTestMixin):
def _test_unicode_data(self, text): def _test_unicode_data(self, text):
......
...@@ -57,7 +57,10 @@ def has_forum_access(uname, course_id, rolename): ...@@ -57,7 +57,10 @@ def has_forum_access(uname, course_id, rolename):
def _get_discussion_modules(course): def _get_discussion_modules(course):
all_modules = modulestore().get_items(course.id, qualifiers={'category': 'discussion'}) discussion_modules = modulestore().get_items(course.id, qualifiers={'category': 'discussion'})
discussion_xblocks = modulestore().get_items(course.id, qualifiers={'category': 'discussion-forum'})
all_discussions = discussion_modules + discussion_xblocks
def has_required_keys(module): def has_required_keys(module):
for key in ('discussion_id', 'discussion_category', 'discussion_target'): for key in ('discussion_id', 'discussion_category', 'discussion_target'):
...@@ -66,7 +69,7 @@ def _get_discussion_modules(course): ...@@ -66,7 +69,7 @@ def _get_discussion_modules(course):
return False return False
return True return True
return filter(has_required_keys, all_modules) return filter(has_required_keys, all_discussions)
def _get_discussion_id_map(course): def _get_discussion_id_map(course):
...@@ -200,6 +203,23 @@ def get_discussion_category_map(course): ...@@ -200,6 +203,23 @@ def get_discussion_category_map(course):
return _filter_unstarted_categories(category_map) return _filter_unstarted_categories(category_map)
def get_discussion_categories_ids(course):
"""
Returns a list of available ids of categories for the course.
"""
ids = []
queue = [get_discussion_category_map(course)]
while queue:
category_map = queue.pop()
for child in category_map["children"]:
if child in category_map["entries"]:
ids.append(category_map["entries"][child]["id"])
else:
queue.append(category_map["subcategories"][child])
return ids
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
def __init__(self, data=None): def __init__(self, data=None):
content = json.dumps(data, cls=i4xEncoder) content = json.dumps(data, cls=i4xEncoder)
......
...@@ -1077,6 +1077,10 @@ PIPELINE_CSS = { ...@@ -1077,6 +1077,10 @@ PIPELINE_CSS = {
], ],
'output_filename': 'css/lms-style-xmodule-annotations.css', 'output_filename': 'css/lms-style-xmodule-annotations.css',
}, },
'discussion': {
'source_filenames': ['sass/discussion-forum.css'],
'output_filename': 'css/lms-style-discussion-forum.css'
}
} }
......
...@@ -22,7 +22,7 @@ class Thread(models.Model): ...@@ -22,7 +22,7 @@ class Thread(models.Model):
updatable_fields = [ updatable_fields = [
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned' 'closed', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned', 'thread_type'
] ]
metric_tag_fields = [ metric_tag_fields = [
......
// Discussion app styles
@import 'bourbon/bourbon';
// base - utilities
@import 'base/variables';
@import 'base/mixins';
@import 'base/extends';
@import 'discussion/utilities/variables';
@import 'discussion/mixins';
// base - elements
@import 'elements/typography';
@import 'shared/modal';
@import 'discussion/discussion';
...@@ -50,7 +50,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -50,7 +50,6 @@ ${page_title_breadcrumbs(course_name())}
<script type="text/javascript" src="${static.url('js/vendor/codemirror-compressed.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/codemirror-compressed.js')}"></script>
<%static:js group='courseware'/> <%static:js group='courseware'/>
<%static:js group='discussion'/>
<%include file="../discussion/_js_body_dependencies.html" /> <%include file="../discussion/_js_body_dependencies.html" />
% if staff_access: % if staff_access:
......
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