Commit 7e12a1f7 by Brian Jacobel Committed by GitHub

Merge pull request #12771 from edx/discussion-forums-ui-update

Discussion forums UI update feature branch
parents 29264007 9b4f3061
/* globals DiscussionThreadListView, DiscussionThreadView, DiscussionUtil, NewPostView, Thread */
(function() {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
function getSingleThreadRoute(commentable_id, thread_id) {
return commentable_id + '/threads/' + thread_id;
}
if (typeof Backbone !== 'undefined' && Backbone !== null) {
this.DiscussionRouter = (function(_super) {
var allThreadsRoute = '',
singleThreadRoute = getSingleThreadRoute(':forum_name', ':thread_id'), // :forum_name/threads/:thread_id
routes = {};
routes[allThreadsRoute] = 'allThreads';
routes[singleThreadRoute] = 'showThread';
__extends(DiscussionRouter, _super);
function DiscussionRouter() {
var self = this;
this.hideNewPost = function() {
return DiscussionRouter.prototype.hideNewPost.apply(self, arguments);
};
this.showNewPost = function() {
return DiscussionRouter.prototype.showNewPost.apply(self, arguments);
};
this.navigateToAllThreads = function() {
return DiscussionRouter.prototype.navigateToAllThreads.apply(self, arguments);
};
this.navigateToThread = function() {
return DiscussionRouter.prototype.navigateToThread.apply(self, arguments);
};
this.showMain = function() {
return DiscussionRouter.prototype.showMain.apply(self, arguments);
};
this.renderThreadView = function() {
return DiscussionRouter.prototype.renderThreadView.apply(self, arguments);
};
this.setActiveThread = function() {
return DiscussionRouter.prototype.setActiveThread.apply(self, arguments);
};
return DiscussionRouter.__super__.constructor.apply(this, arguments);
}
DiscussionRouter.prototype.routes = routes;
DiscussionRouter.prototype.initialize = function(options) {
var self = this;
this.discussion = options.discussion;
this.course_settings = options.course_settings;
this.nav = new DiscussionThreadListView({
collection: this.discussion,
el: $('.forum-nav'),
courseSettings: this.course_settings
});
this.nav.on('thread:selected', this.navigateToThread);
this.nav.on('thread:removed', this.navigateToAllThreads);
this.nav.on('threads:rendered', this.setActiveThread);
this.nav.on('thread:created', this.navigateToThread);
this.nav.render();
this.newPost = $('.new-post-article');
this.newPostView = new NewPostView({
el: this.newPost,
collection: this.discussion,
course_settings: this.course_settings,
mode: 'tab'
});
this.newPostView.render();
this.listenTo(this.newPostView, 'newPost:cancel', this.hideNewPost);
$('.new-post-btn').bind('click', this.showNewPost);
return $('.new-post-btn').bind('keydown', function(event) {
return DiscussionUtil.activateOnSpace(event, self.showNewPost);
});
};
DiscussionRouter.prototype.allThreads = function() {
this.nav.updateSidebar();
this.nav.goHome();
};
DiscussionRouter.prototype.setActiveThread = function() {
if (this.thread) {
this.nav.setActiveThread(this.thread.get('id'));
}
};
DiscussionRouter.prototype.showThread = function(forum_name, thread_id) {
var self = this;
this.thread = this.discussion.get(thread_id);
if (this.thread) {
this.renderThreadView();
return;
}
// if thread is not loaded yet for some reason - try loading it
DiscussionUtil.safeAjax({
url: DiscussionUtil.urlFor('retrieve_single_thread', forum_name, thread_id)
}).done(function(data) {
// if succeded - proceed normally
self.thread = new Thread(data.content);
self.discussion.add(self.thread);
self.renderThreadView();
}).fail(function(xhr) {
// otherwise display error message and navigate to all threads view
var errorMsg;
if (xhr.status === 404) {
errorMsg = gettext('The thread you selected has been deleted. Please select another thread.');
} else {
errorMsg = gettext('We had some trouble loading more responses. Please try again.');
}
DiscussionUtil.discussionAlert(gettext('Sorry'), errorMsg);
this.allThreads();
});
};
DiscussionRouter.prototype.renderThreadView = function() {
this.thread.set('unread_comments_count', 0);
this.thread.set('read', true);
this.setActiveThread();
this.showMain();
};
DiscussionRouter.prototype.showMain = function() {
var self = this;
if (this.main) {
this.main.cleanup();
this.main.undelegateEvents();
}
if (!($('.forum-content').is(':visible'))) {
$('.forum-content').fadeIn();
}
if (this.newPost.is(':visible')) {
this.newPost.fadeOut();
}
this.main = new DiscussionThreadView({
el: $('.forum-content'),
model: this.thread,
mode: 'tab',
course_settings: this.course_settings
});
this.main.render();
this.main.on('thread:responses:rendered', function() {
return self.nav.updateSidebar();
});
this.thread.on('thread:thread_type_updated', this.showMain);
};
DiscussionRouter.prototype.navigateToThread = function(thread_id) {
var thread, targetThreadRoute;
thread = this.discussion.get(thread_id);
targetThreadRoute = getSingleThreadRoute(thread.get('commentable_id'), thread_id);
this.navigate(targetThreadRoute, {trigger: true});
};
DiscussionRouter.prototype.navigateToAllThreads = function() {
this.navigate(allThreadsRoute, {trigger: true});
};
DiscussionRouter.prototype.showNewPost = function() {
var self = this;
$('.forum-content').fadeOut({
duration: 200,
complete: function() {
return self.newPost.fadeIn(200).focus();
}
});
};
DiscussionRouter.prototype.hideNewPost = function() {
this.newPost.fadeOut({
duration: 200,
complete: function() {
return $('.forum-content').fadeIn(200).find('.thread-wrapper').focus();
}
});
};
return DiscussionRouter;
})(Backbone.Router);
}
}).call(window);
/* global $$course_id, Content, Discussion, DiscussionRouter, DiscussionCourseSettings,
DiscussionUser, DiscussionUserProfileView, DiscussionUtil */
(function() {
'use strict';
var DiscussionApp, DiscussionProfileApp;
if (typeof Backbone !== 'undefined' && Backbone !== null) {
DiscussionApp = {
start: function(elem) {
var content_info, course_settings, discussion, element, sort_preference, thread_pages, threads,
user, user_info;
DiscussionUtil.loadRolesFromContainer();
element = $(elem);
window.$$course_id = element.data('course-id');
window.courseName = element.data('course-name');
user_info = element.data('user-info');
sort_preference = element.data('sort-preference');
threads = element.data('threads');
thread_pages = element.data('thread-pages');
content_info = element.data('content-info');
user = new DiscussionUser(user_info);
DiscussionUtil.setUser(user);
window.user = user;
Content.loadContentInfos(content_info);
discussion = new Discussion(threads, {
pages: thread_pages,
sort: sort_preference
});
course_settings = new DiscussionCourseSettings(element.data('course-settings'));
new DiscussionRouter({ // eslint-disable-line no-new
discussion: discussion,
course_settings: course_settings
});
if (!Backbone.History.started) {
Backbone.history.start({pushState: true, root: '/courses/' + $$course_id + '/discussion/forum/'});
} else {
Backbone.history.loadUrl(window.location.pathname);
}
}
};
DiscussionProfileApp = {
start: function(elem) {
var element, numPages, page, threads, user_info;
DiscussionUtil.loadRoles({
'Moderator': [],
'Administrator': [],
'Community TA': []
});
element = $(elem);
window.$$course_id = element.data('course-id');
threads = element.data('threads');
user_info = element.data('user-info');
window.user = new DiscussionUser(user_info);
page = element.data('page');
numPages = element.data('num-pages');
return new DiscussionUserProfileView({
el: element,
collection: threads,
page: page,
numPages: numPages
});
}
};
$(function() {
$('section.discussion').each(function(index, elem) {
return DiscussionApp.start(elem);
});
return $('section.discussion-user-threads').each(function(index, elem) {
return DiscussionProfileApp.start(elem);
});
});
}
}).call(window);
...@@ -23,10 +23,6 @@ ...@@ -23,10 +23,6 @@
this.roleIds = roles; this.roleIds = roles;
}; };
DiscussionUtil.loadRolesFromContainer = function() {
return this.loadRoles($('#discussion-container').data('roles'));
};
DiscussionUtil.isStaff = function(userId) { DiscussionUtil.isStaff = function(userId) {
var staff; var staff;
if (_.isUndefined(userId)) { if (_.isUndefined(userId)) {
......
/* globals Discussion, DiscussionThreadProfileView, DiscussionUtil, URI */
(function() {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
if (typeof Backbone !== 'undefined' && Backbone !== null) {
this.DiscussionUserProfileView = (function(_super) {
__extends(DiscussionUserProfileView, _super);
function DiscussionUserProfileView() {
var self = this;
this.render = function() {
return DiscussionUserProfileView.prototype.render.apply(self, arguments);
};
return DiscussionUserProfileView.__super__.constructor.apply(this, arguments);
}
DiscussionUserProfileView.prototype.events = {
'click .discussion-paginator a': 'changePage'
};
DiscussionUserProfileView.prototype.initialize = function(options) {
DiscussionUserProfileView.__super__.initialize.call(this);
this.page = options.page;
this.numPages = options.numPages;
this.discussion = new Discussion();
this.discussion.on('reset', this.render);
return this.discussion.reset(this.collection, {
silent: false
});
};
DiscussionUserProfileView.prototype.render = function() {
var baseUri, pageUrlFunc, paginationParams,
self = this;
this.$el.html(_.template($('#user-profile-template').html())({
threads: this.discussion.models
}));
this.discussion.map(function(thread) {
return new DiscussionThreadProfileView({
el: self.$('article#thread_' + thread.id),
model: thread
}).render();
});
baseUri = URI(window.location).removeSearch('page');
pageUrlFunc = function(page) {
return baseUri.clone().addSearch('page', page);
};
paginationParams = DiscussionUtil.getPaginationParams(this.page, this.numPages, pageUrlFunc);
this.$el.find('.discussion-pagination')
.html(_.template($('#pagination-template').html())(paginationParams));
};
DiscussionUserProfileView.prototype.changePage = function(event) {
var url,
self = this;
event.preventDefault();
url = $(event.target).attr('href');
return DiscussionUtil.safeAjax({
$elem: this.$el,
$loading: $(event.target),
takeFocus: true,
url: url,
type: 'GET',
dataType: 'json',
success: function(response) {
self.page = response.page;
self.numPages = response.num_pages;
self.discussion.reset(response.discussion_data, {
silent: false
});
history.pushState({}, '', url);
return $('html, body').animate({
scrollTop: 0
});
},
error: function() {
return DiscussionUtil.discussionAlert(
gettext('Sorry'),
gettext('We had some trouble loading the page you requested. Please try again.')
);
}
});
};
return DiscussionUserProfileView;
})(Backbone.View);
}
}).call(window);
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<th scope="row" class="row-title"><%- gettext("Find discussions") %></td> <th scope="row" class="row-title"><%- gettext("Find discussions") %></td>
<td class="row-item"> <td class="row-item">
<span class="icon fa fa-reorder" aria-hidden="true"></span> <span class="icon fa fa-reorder" aria-hidden="true"></span>
<span class="row-description"><%- gettext("Use the Discussion Topics menu to find specific topics.") %></span> <span class="row-description"><%- gettext("Use the All Topics menu to find specific topics.") %></span>
</td> </td>
<td class="row-item"> <td class="row-item">
<span class="icon fa fa-search" aria-hidden="true"></span> <span class="icon fa fa-search" aria-hidden="true"></span>
......
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
) %> ) %>
</p> </p>
<div class="post-labels"> <div class="post-labels">
<span class="post-label-reported"><span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %></span> <span class="post-label post-label-reported">
<span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %>
</span>
</div> </div>
</div> </div>
<li data-id="<%- id %>" class="forum-nav-thread<% if (typeof(read) != "undefined" && !read) { %> is-unread<% } %>"> <li data-id="<%- id %>" class="forum-nav-thread<% if (neverRead) { %> never-read<% } %>">
<a href="#" class="forum-nav-thread-link"> <a href="#" class="forum-nav-thread-link">
<div class="forum-nav-thread-wrapper-0"> <div class="forum-nav-thread-wrapper-0">
<% <%
...@@ -25,31 +25,31 @@ ...@@ -25,31 +25,31 @@
<% if(pinned || subscribed || staff_authored || community_ta_authored) { %> <% if(pinned || subscribed || staff_authored || community_ta_authored) { %>
<ul class="forum-nav-thread-labels"> <ul class="forum-nav-thread-labels">
<% if (pinned) { %> <% if (pinned) { %>
<li class="post-label-pinned"> <li class="post-label post-label-pinned">
<span class="icon fa fa-thumb-tack" aria-hidden="true"></span> <span class="icon fa fa-thumb-tack" aria-hidden="true"></span>
<% // Translators: This is a label for a forum thread that has been pinned %> <% // Translators: This is a label for a forum thread that has been pinned %>
<%- gettext("Pinned") %> <%- gettext("Pinned") %>
</li> </li>
<% } %> <% } %>
<% if (subscribed) { %> <% if (subscribed) { %>
<li class="post-label-following"> <li class="post-label post-label-following">
<span class="icon fa fa-star" aria-hidden="true"></span> <span class="icon fa fa-star" aria-hidden="true"></span>
<% // Translators: This is a label for a forum thread that the user is subscribed to %> <% // Translators: This is a label for a forum thread that the user is subscribed to %>
<%- gettext("Following") %> <%- gettext("Following") %>
</li> </li>
<% } %> <% } %>
<% if (staff_authored) { %> <% if (staff_authored) { %>
<li class="post-label-by-staff"> <li class="post-label post-label-by-staff">
<span class="icon fa fa-user" aria-hidden="true"></span> <span class="icon fa fa-user" aria-hidden="true"></span>
<% // Translators: This is a label for a forum thread that was authored by a member of the course staff %> <% // Translators: This is a label for a forum thread that was authored by a member of the course staff %>
<%- gettext("By: Staff") %> <%- gettext("Staff") %>
</li> </li>
<% } %> <% } %>
<% if (community_ta_authored) { %> <% if (community_ta_authored) { %>
<li class="post-label-by-community-ta"> <li class="post-label post-label-by-community-ta">
<span class="icon fa fa-user" aria-hidden="true"></span> <span class="icon fa fa-user" aria-hidden="true"></span>
<% // Translators: This is a label for a forum thread that was authored by a community TA %> <% // Translators: This is a label for a forum thread that was authored by a community TA %>
<%- gettext("By: Community TA") %> <%- gettext("Community TA") %>
</li> </li>
<% } %> <% } %>
</ul> </ul>
...@@ -72,7 +72,18 @@ ...@@ -72,7 +72,18 @@
%> %>
</span> </span>
<span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>"> <% if (!neverRead && unread_comments_count > 0) { %>
<span class="forum-nav-thread-unread-comments-count">
<%-
StringUtils.interpolate(
gettext('{unread_comments_count} new'),
{unread_comments_count: unread_comments_count}
)
%>
</span>
<% } %>
<span class="forum-nav-thread-comments-count">
<% <%
var fmt; var fmt;
// Counts in data do not include the post itself, but the UI should // Counts in data do not include the post itself, but the UI should
......
...@@ -38,7 +38,9 @@ ...@@ -38,7 +38,9 @@
<% } %> <% } %>
</p> </p>
<div class="post-labels"> <div class="post-labels">
<span class="post-label-reported"><span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %></span> <span class="post-label post-label-reported">
<span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %>
</span>
</div> </div>
</div> </div>
<div class="response-header-actions"> <div class="response-header-actions">
......
...@@ -21,9 +21,15 @@ ...@@ -21,9 +21,15 @@
%> %>
</p> </p>
<div class="post-labels"> <div class="post-labels">
<span class="post-label-pinned"><span class="icon fa fa-thumb-tack" aria-hidden="true"></span><%- gettext("Pinned") %></span> <span class="post-label post-label-pinned">
<span class="post-label-reported"><span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %></span> <span class="icon fa fa-thumb-tack" aria-hidden="true"></span><%- gettext("Pinned") %>
<span class="post-label-closed"><span class="icon fa fa-lock" aria-hidden="true"></span><%- gettext("Closed") %></span> </span>
<span class="post-label post-label-reported">
<span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %>
</span>
<span class="post-label post-label-closed">
<span class="icon fa fa-lock" aria-hidden="true"></span><%- gettext("Closed") %>
</span>
</div> </div>
</div> </div>
<% if (!readOnly) { %> <% if (!readOnly) { %>
......
...@@ -376,6 +376,10 @@ class DiscussionSortPreferencePage(CoursePage): ...@@ -376,6 +376,10 @@ class DiscussionSortPreferencePage(CoursePage):
""" """
return self.q(css="body.discussion .forum-nav-sort-control").present return self.q(css="body.discussion .forum-nav-sort-control").present
def show_all_discussions(self):
""" Show the list of all discussions. """
self.q(css=".all-topics").click()
def get_selected_sort_preference(self): def get_selected_sort_preference(self):
""" """
Return the text of option that is selected for sorting. Return the text of option that is selected for sorting.
...@@ -417,6 +421,10 @@ class DiscussionTabSingleThreadPage(CoursePage): ...@@ -417,6 +421,10 @@ class DiscussionTabSingleThreadPage(CoursePage):
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self.thread_page, name) return getattr(self.thread_page, name)
def show_all_discussions(self):
""" Show the list of all discussions. """
self.q(css=".all-topics").click()
def close_open_thread(self): def close_open_thread(self):
with self.thread_page.secondary_action_menu_open(".thread-main-wrapper"): with self.thread_page.secondary_action_menu_open(".thread-main-wrapper"):
self._find_within(".thread-main-wrapper .action-close").first.click() self._find_within(".thread-main-wrapper .action-close").first.click()
...@@ -435,6 +443,7 @@ class DiscussionTabSingleThreadPage(CoursePage): ...@@ -435,6 +443,7 @@ class DiscussionTabSingleThreadPage(CoursePage):
Click specific thread on the list. Click specific thread on the list.
""" """
thread_selector = "li[data-id='{}']".format(thread_id) thread_selector = "li[data-id='{}']".format(thread_id)
self.show_all_discussions()
self.q(css=thread_selector).first.click() self.q(css=thread_selector).first.click()
EmptyPromise( EmptyPromise(
lambda: self._thread_is_rendered_successfully(thread_id), lambda: self._thread_is_rendered_successfully(thread_id),
...@@ -569,11 +578,11 @@ class DiscussionUserProfilePage(CoursePage): ...@@ -569,11 +578,11 @@ class DiscussionUserProfilePage(CoursePage):
def is_browser_on_page(self): def is_browser_on_page(self):
return ( return (
self.q(css='section.discussion-user-threads[data-course-id="{}"]'.format(self.course_id)).present self.q(css='.discussion-user-threads[data-course-id="{}"]'.format(self.course_id)).present
and and
self.q(css='section.user-profile a.learner-profile-link').present self.q(css='.user-profile .learner-profile-link').present
and and
self.q(css='section.user-profile a.learner-profile-link').text[0] == self.username self.q(css='.user-profile .learner-profile-link').text[0] == self.username
) )
@wait_for_js @wait_for_js
...@@ -670,7 +679,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): ...@@ -670,7 +679,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
return self.q(css=".discussion-body section.home-header").present return self.q(css=".discussion-body section.home-header").present
def perform_search(self, text="dummy"): def perform_search(self, text="dummy"):
self.q(css=".forum-nav-search-input").fill(text + chr(10)) self.q(css=".search-input").fill(text + chr(10))
EmptyPromise( EmptyPromise(
self.is_ajax_finished, self.is_ajax_finished,
"waiting for server to return result" "waiting for server to return result"
...@@ -713,7 +722,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): ...@@ -713,7 +722,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
""" """
Returns the new post button. Returns the new post button.
""" """
elements = self.q(css="ol.course-tabs .new-post-btn") elements = self.q(css=".new-post-btn")
return elements.first if elements.visible and len(elements) == 1 else None return elements.first if elements.visible and len(elements) == 1 else None
@property @property
......
...@@ -218,6 +218,65 @@ class DiscussionHomePageTest(UniqueCourseTest): ...@@ -218,6 +218,65 @@ class DiscussionHomePageTest(UniqueCourseTest):
@attr(shard=2) @attr(shard=2)
class DiscussionNavigationTest(BaseDiscussionTestCase):
"""
Tests for breadcrumbs navigation in the Discussions page nav bar
"""
def setUp(self):
super(DiscussionNavigationTest, self).setUp()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
thread_id = "test_thread_{}".format(uuid4().hex)
thread_fixture = SingleThreadViewFixture(
Thread(
id=thread_id,
body=THREAD_CONTENT_WITH_LATEX,
commentable_id=self.discussion_id
)
)
thread_fixture.push()
self.thread_page = DiscussionTabSingleThreadPage(
self.browser,
self.course_id,
self.discussion_id,
thread_id
)
self.thread_page.visit()
def test_breadcrumbs_push_topic(self):
topic_button = self.thread_page.q(
css=".forum-nav-browse-menu-item[data-discussion-id='{}']".format(self.discussion_id)
)
self.assertTrue(topic_button.visible)
topic_button.click()
# Verify the thread's topic has been pushed to breadcrumbs
breadcrumbs = self.thread_page.q(css=".breadcrumbs .nav-item")
self.assertEqual(len(breadcrumbs), 2)
self.assertEqual(breadcrumbs[1].text, "Test Discussion Topic")
def test_breadcrumbs_back_to_all_topics(self):
topic_button = self.thread_page.q(
css=".forum-nav-browse-menu-item[data-discussion-id='{}']".format(self.discussion_id)
)
self.assertTrue(topic_button.visible)
topic_button.click()
# Verify clicking the first breadcrumb takes you back to all topics
self.thread_page.q(css=".breadcrumbs .nav-item")[0].click()
self.assertEqual(len(self.thread_page.q(css=".breadcrumbs .nav-item")), 1)
def test_breadcrumbs_clear_search(self):
self.thread_page.q(css=".search-input").fill("search text")
self.thread_page.q(css=".search-btn").click()
# Verify that clicking the first breadcrumb clears your search
self.thread_page.q(css=".breadcrumbs .nav-item")[0].click()
self.assertEqual(self.thread_page.q(css=".search-input").text[0], "")
@attr(shard=2)
class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePaginationTestMixin): class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePaginationTestMixin):
""" """
Tests for the discussion page displaying a single thread Tests for the discussion page displaying a single thread
...@@ -1036,7 +1095,7 @@ class DiscussionUserProfileTest(UniqueCourseTest): ...@@ -1036,7 +1095,7 @@ class DiscussionUserProfileTest(UniqueCourseTest):
Tests for user profile page in discussion tab. Tests for user profile page in discussion tab.
""" """
PAGE_SIZE = 20 # django_comment_client.forum.views.THREADS_PER_PAGE PAGE_SIZE = 20 # discussion.views.THREADS_PER_PAGE
PROFILED_USERNAME = "profiled-user" PROFILED_USERNAME = "profiled-user"
def setUp(self): def setUp(self):
...@@ -1261,6 +1320,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest): ...@@ -1261,6 +1320,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
self.sort_page = DiscussionSortPreferencePage(self.browser, self.course_id) self.sort_page = DiscussionSortPreferencePage(self.browser, self.course_id)
self.sort_page.visit() self.sort_page.visit()
self.sort_page.show_all_discussions()
def test_default_sort_preference(self): def test_default_sort_preference(self):
""" """
...@@ -1293,5 +1353,6 @@ class DiscussionSortPreferenceTest(UniqueCourseTest): ...@@ -1293,5 +1353,6 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
selected_sort = self.sort_page.get_selected_sort_preference() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type) self.assertEqual(selected_sort, sort_type)
self.sort_page.refresh_page() self.sort_page.refresh_page()
self.sort_page.show_all_discussions()
selected_sort = self.sort_page.get_selected_sort_preference() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type) self.assertEqual(selected_sort, sort_type)
...@@ -754,7 +754,7 @@ class DiscussionLinkTestCase(TabTestCase): ...@@ -754,7 +754,7 @@ class DiscussionLinkTestCase(TabTestCase):
"""Custom reverse function""" """Custom reverse function"""
def reverse_discussion_link(viewname, args): def reverse_discussion_link(viewname, args):
"""reverse lookup for discussion link""" """reverse lookup for discussion link"""
if viewname == "django_comment_client.forum.views.forum_form_discussion" and args == [unicode(course.id)]: if viewname == "discussion.views.forum_form_discussion" and args == [unicode(course.id)]:
return "default_discussion_link" return "default_discussion_link"
return reverse_discussion_link return reverse_discussion_link
......
"""
Views handling read (GET) requests for the Discussion tab and inline discussions.
"""
from django.conf import settings
from django.utils.translation import ugettext_noop
from courseware.tabs import EnrolledTab
import django_comment_client.utils as utils
class DiscussionTab(EnrolledTab):
"""
A tab for the cs_comments_service forums.
"""
type = 'discussion'
title = ugettext_noop('Discussion')
priority = None
view_name = 'discussion.views.forum_form_discussion'
is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
is_default = False
@classmethod
def is_enabled(cls, course, user=None):
if not super(DiscussionTab, cls).is_enabled(course, user):
return False
return utils.is_discussion_enabled(course.id)
(function(define) {
'use strict';
define(
[
'jquery',
'backbone',
'discussion/js/discussion_router',
'discussion/js/views/discussion_fake_breadcrumbs',
'discussion/js/views/discussion_search_view',
'common/js/discussion/views/new_post_view'
],
function($, Backbone, DiscussionRouter, DiscussionFakeBreadcrumbs, DiscussionSearchView, NewPostView) {
return function(options) {
var userInfo = options.user_info,
sortPreference = options.sort_preference,
threads = options.threads,
threadPages = options.thread_pages,
contentInfo = options.content_info,
user = new window.DiscussionUser(userInfo),
discussion,
courseSettings,
newPostView,
router,
breadcrumbs,
BreadcrumbsModel,
searchBox,
routerEvents;
// TODO: Perhaps eliminate usage of global variables when possible
window.DiscussionUtil.loadRoles(options.roles);
window.$$course_id = options.courseId;
window.courseName = options.course_name;
window.DiscussionUtil.setUser(user);
window.user = user;
window.Content.loadContentInfos(contentInfo);
discussion = new window.Discussion(threads, {pages: threadPages, sort: sortPreference});
courseSettings = new window.DiscussionCourseSettings(options.course_settings);
// Create the new post view
newPostView = new NewPostView({
el: $('.new-post-article'),
collection: discussion,
course_settings: courseSettings,
mode: 'tab'
});
newPostView.render();
// Set up the router to manage the page's history
router = new DiscussionRouter({
courseId: options.courseId,
discussion: discussion,
courseSettings: courseSettings,
newPostView: newPostView
});
router.start();
// Initialize and render search box
searchBox = new DiscussionSearchView({
el: $('.forum-search'),
threadListView: router.nav
}).render();
// Initialize and render breadcrumbs
BreadcrumbsModel = Backbone.Model.extend({
defaults: {
contents: []
}
});
breadcrumbs = new DiscussionFakeBreadcrumbs({
el: $('.has-breadcrumbs'),
model: new BreadcrumbsModel(),
events: {
'click .all-topics': function(event) {
event.preventDefault();
searchBox.clearSearch();
this.model.set('contents', []);
router.navigate('', {trigger: true});
router.nav.toggleBrowseMenu(event);
}
}
}).render();
routerEvents = {
// Add new breadcrumbs and clear search box when the user selects topics
'topic:selected': function(topic) {
breadcrumbs.model.set('contents', topic);
},
// Clear search box when a thread is selected
'thread:selected': function() {
searchBox.clearSearch();
},
// Add 'Search Results' to breadcrumbs when user searches
'search:initiated': function() {
breadcrumbs.model.set('contents', ['Search Results']);
}
};
Object.keys(routerEvents).forEach(function(key) {
router.nav.on(key, routerEvents[key]);
});
};
});
}).call(this, define || RequireJS.define);
(function(define) {
'use strict';
define(['jquery', 'discussion/js/views/discussion_user_profile_view'],
function($, DiscussionUserProfileView) {
return function(options) {
var $element = options.$el,
threads = options.threads,
userInfo = options.userInfo,
page = options.page,
numPages = options.numPages;
// Roles are not included in user profile page, but they are not used for anything
window.DiscussionUtil.loadRoles({
Moderator: [],
Administrator: [],
'Community TA': []
});
window.$$course_id = options.courseId;
window.user = new window.DiscussionUser(userInfo);
new DiscussionUserProfileView({ // eslint-disable-line no-new
el: $element,
collection: threads,
page: page,
numPages: numPages
});
};
});
}).call(this, define || RequireJS.define);
(function(define) {
'use strict';
define(
[
'underscore',
'backbone',
'common/js/discussion/utils',
'common/js/discussion/views/discussion_thread_list_view',
'common/js/discussion/views/discussion_thread_view'
],
function(_, Backbone, DiscussionUtil, DiscussionThreadListView, DiscussionThreadView) {
var DiscussionRouter = Backbone.Router.extend({
routes: {
'': 'allThreads',
':forum_name/threads/:thread_id': 'showThread'
},
initialize: function(options) {
Backbone.Router.prototype.initialize.call(this);
_.bindAll(this, 'allThreads', 'showThread');
this.courseId = options.courseId;
this.discussion = options.discussion;
this.course_settings = options.courseSettings;
this.newPostView = options.newPostView;
this.nav = new DiscussionThreadListView({
collection: this.discussion,
el: $('.forum-nav'),
courseSettings: this.course_settings
});
this.nav.render();
},
start: function() {
var self = this,
$newPostButton = $('.new-post-btn');
this.listenTo(this.newPostView, 'newPost:cancel', this.hideNewPost);
$newPostButton.bind('click', _.bind(this.showNewPost, this));
$newPostButton.bind('keydown', function(event) {
DiscussionUtil.activateOnSpace(event, self.showNewPost);
});
// Automatically navigate when the user selects threads
this.nav.on('thread:selected', _.bind(this.navigateToThread, this));
this.nav.on('thread:removed', _.bind(this.navigateToAllThreads, this));
this.nav.on('threads:rendered', _.bind(this.setActiveThread, this));
this.nav.on('thread:created', _.bind(this.navigateToThread, this));
Backbone.history.start({
pushState: true,
root: '/courses/' + this.courseId + '/discussion/forum/'
});
},
stop: function() {
Backbone.history.stop();
},
allThreads: function() {
this.nav.updateSidebar();
return this.nav.goHome();
},
setActiveThread: function() {
if (this.thread) {
return this.nav.setActiveThread(this.thread.get('id'));
} else {
return this.nav.goHome;
}
},
showThread: function(forumName, threadId) {
this.thread = this.discussion.get(threadId);
this.thread.set('unread_comments_count', 0);
this.thread.set('read', true);
this.setActiveThread();
return this.showMain();
},
showMain: function() {
var self = this;
if (this.main) {
this.main.cleanup();
this.main.undelegateEvents();
}
if (!($('.forum-content').is(':visible'))) {
$('.forum-content').fadeIn();
}
if (this.newPostView.$el.is(':visible')) {
this.newPostView.$el.fadeOut();
}
this.main = new DiscussionThreadView({
el: $('.forum-content'),
model: this.thread,
mode: 'tab',
course_settings: this.course_settings
});
this.main.render();
this.main.on('thread:responses:rendered', function() {
return self.nav.updateSidebar();
});
return this.thread.on('thread:thread_type_updated', this.showMain);
},
navigateToThread: function(threadId) {
var thread;
thread = this.discussion.get(threadId);
return this.navigate('' + (thread.get('commentable_id')) + '/threads/' + threadId, {
trigger: true
});
},
navigateToAllThreads: function() {
return this.navigate('', {
trigger: true
});
},
showNewPost: function() {
var self = this;
return $('.forum-content').fadeOut({
duration: 200,
complete: function() {
return self.newPostView.$el.fadeIn(200).focus();
}
});
},
hideNewPost: function() {
return this.newPostView.$el.fadeOut({
duration: 200,
complete: function() {
return $('.forum-content').fadeIn(200).find('.thread-wrapper')
.focus();
}
});
}
});
return DiscussionRouter;
});
}).call(this, define || RequireJS.define);
define(
[
'jquery',
'backbone',
'common/js/spec_helpers/page_helpers',
'common/js/spec_helpers/discussion_spec_helper',
'discussion/js/discussion_board_factory'
],
function($, Backbone, PageHelpers, DiscussionSpecHelper, DiscussionBoardFactory) {
'use strict';
// TODO: re-enable when this doesn't interact badly with other history tests
xdescribe('Discussion Board Factory', function() {
var initializeDiscussionBoardFactory = function() {
DiscussionBoardFactory({
el: $('.discussion-board'),
courseId: 'test_course_id',
course_name: 'Test Course',
user_info: DiscussionSpecHelper.getTestUserInfo(),
roles: DiscussionSpecHelper.getTestRoleInfo(),
sort_preference: null,
threads: [],
thread_pages: [],
content_info: null,
course_settings: {
is_cohorted: false,
allow_anonymous: false,
allow_anonymous_to_peers: false,
cohorts: [],
category_map: {}
}
});
};
beforeEach(function() {
PageHelpers.preventBackboneChangingUrl();
// Install the fixtures
setFixtures(
'<div class="discussion-board">' +
' <div class="forum-nav"></div>' +
'</div>'
);
DiscussionSpecHelper.setUnderscoreFixtures();
});
afterEach(function() {
Backbone.history.stop();
});
it('can render itself', function() {
initializeDiscussionBoardFactory();
expect($('.discussion-board').text()).toContain('All Discussions');
});
});
}
);
define(
[
'underscore',
'jquery',
'backbone',
'common/js/spec_helpers/discussion_spec_helper',
'discussion/js/discussion_profile_page_factory'
],
function(_, $, Backbone, DiscussionSpecHelper, DiscussionProfilePageFactory) {
'use strict';
describe('Discussion Profile Page Factory', function() {
var testCourseId = 'test_course',
initializeDiscussionProfilePageFactory = function(options) {
DiscussionProfilePageFactory(_.extend(
{
courseId: testCourseId,
$el: $('.discussion-user-threads'),
user_info: DiscussionSpecHelper.getTestUserInfo(),
roles: DiscussionSpecHelper.getTestRoleInfo(),
sort_preference: null,
threads: [],
page: 1,
numPages: 5
},
options
));
};
beforeEach(function() {
setFixtures('<div class="discussion-user-threads"></div>');
DiscussionSpecHelper.setUnderscoreFixtures();
});
it('can render itself', function() {
initializeDiscussionProfilePageFactory();
expect($('.discussion-user-threads').text()).toContain('Active Threads');
});
});
}
);
define([
'jquery',
'edx-ui-toolkit/js/utils/constants',
'discussion/js/views/discussion_search_view'
],
function($, constants, DiscussionSearchView) {
'use strict';
describe('DiscussionSearchView', function() {
var view;
beforeEach(function() {
setFixtures('<div class="search-container"></div>');
view = new DiscussionSearchView({
el: $('.search-container'),
threadListView: {
performSearch: jasmine.createSpy()
}
}).render();
});
describe('Search events', function() {
it('perform search when enter pressed inside search textfield', function() {
view.$el.find('.search-input').trigger($.Event('keydown', {
which: constants.keyCodes.enter
}));
expect(view.threadListView.performSearch).toHaveBeenCalled();
});
it('perform search when search icon is clicked', function() {
view.$el.find('.search-btn').click();
expect(view.threadListView.performSearch).toHaveBeenCalled();
});
});
});
}
);
/**
* This Backbone view mimics the appearance of breadcrumbs, but does not provide true breadcrumb navigation.
* This implementation is a stopgap developed due to limitations in the Discussions UI.
* Don't use this breadcrumbs implementation as a model or reference.
* Instead, check out the UXPL's breadcrumbs, which have been vetted for UX and A11Y.
* http://ux.edx.org/components/breadcrumbs/
*/
(function(define) {
'use strict';
define([
'backbone',
'gettext',
'edx-ui-toolkit/js/utils/html-utils',
'text!discussion/templates/fake-breadcrumbs.underscore'
],
function(Backbone, gettext, HtmlUtils, breadcrumbsTemplate) {
var DiscussionFakeBreadcrumbs = Backbone.View.extend({
initialize: function() {
this.template = HtmlUtils.template(breadcrumbsTemplate);
this.listenTo(this.model, 'change', this.render);
this.render();
},
render: function() {
var json = this.model.attributes;
HtmlUtils.setHtml(this.$el, this.template(json));
return this;
}
});
return DiscussionFakeBreadcrumbs;
});
}).call(this, define || RequireJS.define);
(function(define) {
'use strict';
define([
'underscore',
'backbone',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/constants',
'text!discussion/templates/search.underscore'
],
function(_, Backbone, HtmlUtils, constants, searchTemplate) {
/*
* TODO: Much of the actual search functionality still takes place in discussion_thread_list_view.js
* Because of how it's structured there, extracting it is a massive task. Significant refactoring is needed
* in order to clean up that file and make it possible to break its logic into files like this one.
*/
var searchView = Backbone.View.extend({
events: {
'keydown .search-input': 'performSearch',
'click .search-btn': 'performSearch',
'topic:selected': 'clearSearch'
},
initialize: function(options) {
_.extend(this, _.pick(options, 'threadListView'));
this.template = HtmlUtils.template(searchTemplate);
this.threadListView = options.threadListView;
this.listenTo(this.model, 'change', this.render);
this.render();
},
render: function() {
HtmlUtils.setHtml(this.$el, this.template());
return this;
},
performSearch: function(event) {
if (event.which === constants.keyCodes.enter || event.type === 'click') {
event.preventDefault();
this.threadListView.performSearch($('.search-input', this.$el));
}
},
clearSearch: function() {
this.$('.search-input').val('');
this.threadListView.clearSearchAlerts();
}
});
return searchView;
});
}).call(this, define || RequireJS.define);
(function(define) {
'use strict';
define([
'underscore',
'jquery',
'backbone',
'gettext',
'URI',
'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/utils/view_utils',
'common/js/discussion/discussion',
'common/js/discussion/utils',
'common/js/discussion/views/discussion_thread_profile_view',
'text!discussion/templates/user-profile.underscore',
'text!common/templates/discussion/pagination.underscore'
],
function(_, $, Backbone, gettext, URI, HtmlUtils, ViewUtils, Discussion, DiscussionUtil,
DiscussionThreadProfileView, userProfileTemplate, paginationTemplate) {
var DiscussionUserProfileView = Backbone.View.extend({
events: {
'click .discussion-paginator a': 'changePage'
},
initialize: function(options) {
Backbone.View.prototype.initialize.call(this);
this.page = options.page;
this.numPages = options.numPages;
this.discussion = new Discussion();
this.discussion.on('reset', _.bind(this.render, this));
this.discussion.reset(this.collection, {silent: false});
},
render: function() {
var self = this,
baseUri = URI(window.location).removeSearch('page'),
pageUrlFunc,
paginationParams;
HtmlUtils.setHtml(this.$el, HtmlUtils.template(userProfileTemplate)({
threads: self.discussion.models
}));
this.discussion.map(function(thread) {
var view = new DiscussionThreadProfileView({
el: self.$('article#thread_' + thread.id),
model: thread
});
view.render();
return view;
});
pageUrlFunc = function(page) {
return baseUri.clone().addSearch('page', page).toString();
};
paginationParams = DiscussionUtil.getPaginationParams(this.page, this.numPages, pageUrlFunc);
HtmlUtils.setHtml(
this.$el.find('.discussion-pagination'),
HtmlUtils.template(paginationTemplate)(paginationParams)
);
return this;
},
changePage: function(event) {
var self = this,
url;
event.preventDefault();
url = $(event.target).attr('href');
DiscussionUtil.safeAjax({
$elem: this.$el,
$loading: $(event.target),
takeFocus: true,
url: url,
type: 'GET',
dataType: 'json',
success: function(response) {
self.page = response.page;
self.numPages = response.num_pages;
self.discussion.reset(response.discussion_data, {silent: false});
history.pushState({}, '', url);
ViewUtils.setScrollTop(0);
},
error: function() {
DiscussionUtil.discussionAlert(
gettext('Sorry'),
gettext('We had some trouble loading the page you requested. Please try again.')
);
}
});
}
});
return DiscussionUserProfileView;
});
}).call(this, define || RequireJS.define);
<h6 class="hd-6 breadcrumbs">
<span class="nav-item">
<a class="all-topics" href="">
<span class="icon fa fa-bars" aria-hidden="true"></span><%- gettext('All Topics') %>
</a>
</span>
<% contents.forEach(function(content) { %>
<span class="fa fa-angle-right"></span>
<span class="nav-item"><%- content %></span>
<% }); %>
</h6>
<label class="field-label sr-only" for="search" id="search-hint"><%- gettext("Search all posts") %></label>
<input
class="field-input input-text search-input"
type="search"
name="search"
id="search"
placeholder="<%- gettext("Search all posts") %>"
/>
<button class="btn-brand btn-small search-btn" type="button"><%- gettext("Search") %></button>
<h2><%- gettext("Active Threads") %></h2> <h2><%- gettext("Active Threads") %></h2>
<section class="discussion"> <section class="discussion">
<% _.each(threads, function(thread) { %> <% _.each(threads, function(thread) { %>
<article class="discussion-thread" id="thread_<%= thread.id %>"/> <article class="discussion-thread" id="thread_<%- thread.id %>"/>
<% }); %> <% }); %>
</section> </section>
<section class="discussion-pagination"/> <section class="discussion-pagination"/>
## mako
<%! main_css = "style-discussion-main" %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
%>
<%block name="bodyclass">discussion</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra">
<%include file="../discussion/_js_head_dependencies.html" />
</%block>
<%block name="base_js_dependencies">
## Enable fast preview to fix discussion MathJax rendering bug when page first loads.
<%include file="/discussion/_js_body_dependencies.html" args="disable_fast_preview=False"/>
</%block>
<%block name="js_extra">
<%static:require_module module_name="discussion/js/discussion_board_factory" class_name="DiscussionBoardFactory">
DiscussionBoardFactory({
courseId: '${unicode(course.id) | n, js_escaped_string}',
$el: $(".discussion-board"),
user_info: ${user_info | n, dump_js_escaped_json},
roles: ${roles | n, dump_js_escaped_json},
sort_preference: '${sort_preference | n, js_escaped_string}',
threads: ${threads | n, dump_js_escaped_json},
thread_pages: '${thread_pages | n, js_escaped_string}',
content_info: ${annotated_content_info | n, dump_js_escaped_json},
course_name: '${course.display_name_with_default | n, js_escaped_string}',
course_settings: ${course_settings | n, dump_js_escaped_json}
});
</%static:require_module>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<%block name="content">
<section class="discussion discussion-board container" id="discussion-container"
data-course-id="${course_id}"
data-user-create-comment="${can_create_comment}"
data-user-create-subcomment="${can_create_subcomment}"
data-read-only="false"
data-sort-preference="${sort_preference}"
data-flag-moderator="${flag_moderator}"
data-user-cohort-id="${user_cohort}">
<header class="page-header has-secondary">
## Breadcrumb navigation
<div class="page-header-main">
<div class="sr-is-focusable" tabindex="-1"></div>
<div class="has-breadcrumbs"></div>
</div>
<div class="page-header-secondary">
## Add Post button
% if has_permission(user, 'create_thread', course.id):
<div class="form-actions">
<button class="btn btn-small new-post-btn">${_("Add a Post")}</button>
</div>
% endif
## Search box
<div class="forum-search"></div>
</div>
</header>
<div class="page-content">
<div class="discussion-body layout layout-1t2t">
<aside class="forum-nav layout-col layout-col-a" role="complementary" aria-label="${_("Discussion thread list")}">
</aside>
<main id="main" aria-label="Content" tabindex="-1" class="discussion-column layout-col layout-col-b">
<article class="new-post-article" style="display: none" tabindex="-1" aria-label="${_("New topic form")}"></article>
<div class="forum-content"></div>
</main>
</div>
</div>
</section>
</%block>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
## mako
<%! main_css = "style-discussion-main" %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
%>
<%block name="bodyclass">discussion-user-profile</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra">
<%include file="_js_head_dependencies.html" />
</%block>
<%block name="js_extra">
<%include file="_js_body_dependencies.html" />
<%static:require_module module_name="discussion/js/discussion_profile_page_factory" class_name="DiscussionProfilePageFactory">
<%
profile_page_context = {
'courseId': unicode(course.id),
'courseName': course.display_name_with_default,
'userInfo': user_info,
'threads': threads,
'page': page,
'numPages': num_pages,
}
%>
DiscussionProfilePageFactory(_.extend(
{
$el: $('.discussion-user-threads')
},
${profile_page_context | n, dump_js_escaped_json}
));
</%static:require_module>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<header class="page-header">
<div class="page-header-main">
<div class="sr-is-focusable" tabindex="-1"></div>
<h2 class="hd hd-2 page-title">${_("Discussion")}</h2>
</div>
</header>
<div class="page-content">
<div class="layout layout-1t2t">
<aside class="forum-nav layout-col layout-col-a" role="complementary" aria-label="${_("Discussion thread list")}">
<nav class="user-profile" aria-label="${_('User Profile')}">
<article class="sidebar-module discussion-sidebar">
<%include file="_user_profile.html" />
</article>
</nav>
</aside>
<main id="main" aria-label="Content" tabindex="-1" class="discussion-column layout-col layout-col-b">
<div class="course-content discussion-user-threads" data-course-id="${course.id}"
data-course-name="${course.display_name_with_default}"
data-threads="${threads}" data-user-info="${user_info}"
data-page="${page}" data-num-pages="${num_pages}">
</div>
</main>
</div>
</div>
</section>
<%include file="_underscore_templates.html" />
## mako
<%! main_css = "style-discussion-main" %>
<%! from django.utils.translation import ugettext as _ %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%block name="bodyclass">discussion</%block>
<%block name="headextra">
<%include file="../discussion/_js_head_dependencies.html" />
</%block>
<%block name="content">
<h2>${_("Discussion unavailable")}</h2>
<div class="alert alert-error" role="alert" aria-labelledby="alert-title-error" tabindex="-1">
<span class="icon alert-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<div class="alert-message-with-action">
<p class="alert-copy">
${_("The discussions are currently undergoing maintenance. We'll have them back up shortly!")}
</p>
</div>
</div>
</%block>
...@@ -10,7 +10,6 @@ from django.utils import translation ...@@ -10,7 +10,6 @@ from django.utils import translation
from lms.lib.comment_client.utils import CommentClientPaginatedResult from lms.lib.comment_client.utils import CommentClientPaginatedResult
from django_comment_common.utils import ThreadContext from django_comment_common.utils import ThreadContext
from django_comment_client.forum import views
from django_comment_client.permissions import get_team from django_comment_client.permissions import get_team
from django_comment_client.tests.group_id import ( from django_comment_client.tests.group_id import (
CohortedTopicGroupIdTestMixin, CohortedTopicGroupIdTestMixin,
...@@ -19,6 +18,7 @@ from django_comment_client.tests.group_id import ( ...@@ -19,6 +18,7 @@ from django_comment_client.tests.group_id import (
from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import CohortedTestCase from django_comment_client.tests.utils import CohortedTestCase
from django_comment_client.utils import strip_none from django_comment_client.utils import strip_none
from lms.djangoapps.discussion import views
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from openedx.core.djangoapps.util.testing import ContentGroupTestCase from openedx.core.djangoapps.util.testing import ContentGroupTestCase
...@@ -87,7 +87,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -87,7 +87,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
# that gets the current user's info # that gets the current user's info
mock_from_django_user.return_value = Mock() mock_from_django_user.return_value = Mock()
url = reverse('django_comment_client.forum.views.user_profile', url = reverse('discussion.views.user_profile',
kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345 kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345
self.response = self.client.get(url) self.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404) self.assertEqual(self.response.status_code, 404)
...@@ -104,7 +104,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -104,7 +104,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
# that gets the current user's info # that gets the current user's info
mock_from_django_user.return_value = Mock() mock_from_django_user.return_value = Mock()
url = reverse('django_comment_client.forum.views.followed_threads', url = reverse('discussion.views.followed_threads',
kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345 kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345
self.response = self.client.get(url) self.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404) self.assertEqual(self.response.status_code, 404)
...@@ -459,7 +459,7 @@ class SingleCohortedThreadTestCase(CohortedTestCase): ...@@ -459,7 +459,7 @@ class SingleCohortedThreadTestCase(CohortedTestCase):
html = response.content html = response.content
# Verify that the group name is correctly included in the HTML # Verify that the group name is correctly included in the HTML
self.assertRegexpMatches(html, r'&#34;group_name&#34;: &#34;student_cohort&#34;') self.assertRegexpMatches(html, r'"group_name": "student_cohort"')
@patch('lms.lib.comment_client.utils.requests.request', autospec=True) @patch('lms.lib.comment_client.utils.requests.request', autospec=True)
...@@ -812,7 +812,7 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT ...@@ -812,7 +812,7 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT
self.client.login(username=user.username, password='test') self.client.login(username=user.username, password='test')
return self.client.get( return self.client.get(
reverse(views.forum_form_discussion, args=[unicode(self.course.id)]), reverse("discussion.views.forum_form_discussion", args=[unicode(self.course.id)]),
data=request_data, data=request_data,
**headers **headers
) )
...@@ -1147,10 +1147,10 @@ class UserProfileTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -1147,10 +1147,10 @@ class UserProfileTestCase(UrlResetMixin, ModuleStoreTestCase):
self.assertRegexpMatches(html, r'data-num-pages="1"') self.assertRegexpMatches(html, r'data-num-pages="1"')
self.assertRegexpMatches(html, r'<span>1</span> discussion started') self.assertRegexpMatches(html, r'<span>1</span> discussion started')
self.assertRegexpMatches(html, r'<span>2</span> comments') self.assertRegexpMatches(html, r'<span>2</span> comments')
self.assertRegexpMatches(html, r'&#34;id&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_ID)) self.assertRegexpMatches(html, r'&#39;id&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_ID))
self.assertRegexpMatches(html, r'&#34;title&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT)) self.assertRegexpMatches(html, r'&#39;title&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;body&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT)) self.assertRegexpMatches(html, r'&#39;body&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;username&#34;: &#34;{}&#34;'.format(self.student.username)) self.assertRegexpMatches(html, r'&#39;username&#39;: u&#39;{}&#39;'.format(self.student.username))
def check_ajax(self, mock_request, **params): def check_ajax(self, mock_request, **params):
response = self.get_response(mock_request, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest") response = self.get_response(mock_request, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
...@@ -1257,7 +1257,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -1257,7 +1257,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
self.client.get( self.client.get(
reverse( reverse(
"django_comment_client.forum.views.single_thread", "discussion.views.single_thread",
kwargs={ kwargs={
"course_id": self.course.id.to_deprecated_string(), "course_id": self.course.id.to_deprecated_string(),
"discussion_id": "dummy_discussion_id", "discussion_id": "dummy_discussion_id",
...@@ -1274,7 +1274,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -1274,7 +1274,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
self.client.get( self.client.get(
reverse( reverse(
"django_comment_client.forum.views.forum_form_discussion", "discussion.views.forum_form_discussion",
kwargs={"course_id": self.course.id.to_deprecated_string()} kwargs={"course_id": self.course.id.to_deprecated_string()}
), ),
) )
...@@ -1360,8 +1360,9 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -1360,8 +1360,9 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase):
""" """
Test that XSS attack is prevented Test that XSS attack is prevented
""" """
mock_user.return_value.to_dict.return_value = {}
reverse_url = "%s%s" % (reverse( reverse_url = "%s%s" % (reverse(
"django_comment_client.forum.views.forum_form_discussion", "discussion.views.forum_form_discussion",
kwargs={"course_id": unicode(self.course.id)}), '/forum_form_discussion') kwargs={"course_id": unicode(self.course.id)}), '/forum_form_discussion')
# Test that malicious code does not appear in html # Test that malicious code does not appear in html
url = "%s?%s=%s" % (reverse_url, 'sort_key', malicious_code) url = "%s?%s=%s" % (reverse_url, 'sort_key', malicious_code)
...@@ -1377,10 +1378,10 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -1377,10 +1378,10 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase):
Test that XSS attack is prevented Test that XSS attack is prevented
""" """
mock_threads.return_value = [], 1, 1 mock_threads.return_value = [], 1, 1
mock_from_django_user.return_value = Mock() mock_from_django_user.return_value.to_dict.return_value = {}
mock_request.side_effect = make_mock_request_impl(course=self.course, text='dummy') mock_request.side_effect = make_mock_request_impl(course=self.course, text='dummy')
url = reverse('django_comment_client.forum.views.user_profile', url = reverse('discussion.views.user_profile',
kwargs={'course_id': unicode(self.course.id), 'user_id': str(self.student.id)}) kwargs={'course_id': unicode(self.course.id), 'user_id': str(self.student.id)})
# Test that malicious code does not appear in html # Test that malicious code does not appear in html
url_string = "%s?%s=%s" % (url, 'page', malicious_code) url_string = "%s?%s=%s" % (url, 'page', malicious_code)
......
...@@ -4,7 +4,7 @@ Forum urls for the django_comment_client. ...@@ -4,7 +4,7 @@ Forum urls for the django_comment_client.
from django.conf.urls import url, patterns from django.conf.urls import url, patterns
urlpatterns = patterns( urlpatterns = patterns(
'django_comment_client.forum.views', 'discussion.views',
url(r'users/(?P<user_id>\w+)/followed$', 'followed_threads', name='followed_threads'), url(r'users/(?P<user_id>\w+)/followed$', 'followed_threads', name='followed_threads'),
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'), url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
......
...@@ -3,27 +3,23 @@ Views handling read (GET) requests for the Discussion tab and inline discussions ...@@ -3,27 +3,23 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
""" """
from functools import wraps from functools import wraps
import json
import logging import logging
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext_noop from django.shortcuts import render_to_response
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
import newrelic.agent import newrelic.agent
from edxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from openedx.core.djangoapps.course_groups.cohorts import ( from openedx.core.djangoapps.course_groups.cohorts import (
is_course_cohorted, is_course_cohorted,
get_cohort_id, get_cohort_id,
get_course_cohorts, get_course_cohorts,
) )
from courseware.tabs import EnrolledTab
from courseware.access import has_access from courseware.access import has_access
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -48,25 +44,6 @@ PAGES_NEARBY_DELTA = 2 ...@@ -48,25 +44,6 @@ PAGES_NEARBY_DELTA = 2
log = logging.getLogger("edx.discussions") log = logging.getLogger("edx.discussions")
class DiscussionTab(EnrolledTab):
"""
A tab for the cs_comments_service forums.
"""
type = 'discussion'
title = ugettext_noop('Discussion')
priority = None
view_name = 'django_comment_client.forum.views.forum_form_discussion'
is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
is_default = False
@classmethod
def is_enabled(cls, course, user=None):
if not super(DiscussionTab, cls).is_enabled(course, user):
return False
return utils.is_discussion_enabled(course.id)
@newrelic.agent.function_trace() @newrelic.agent.function_trace()
def make_course_settings(course, user): def make_course_settings(course, user):
""" """
...@@ -115,7 +92,8 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): ...@@ -115,7 +92,8 @@ def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
# If the user did not select a sort key, use their last used sort key # If the user did not select a sort key, use their last used sort key
cc_user = cc.User.from_django_user(request.user) cc_user = cc.User.from_django_user(request.user)
cc_user.retrieve() cc_user.retrieve()
# TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value # TODO: After the comment service is updated this can just be
# user.default_sort_key because the service returns the default value
default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key'] default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key']
else: else:
# If the user clicked a sort key, update their default sort key # If the user clicked a sort key, update their default sort key
...@@ -239,7 +217,10 @@ def forum_form_discussion(request, course_key): ...@@ -239,7 +217,10 @@ def forum_form_discussion(request, course_key):
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
except cc.utils.CommentClientMaintenanceError: except cc.utils.CommentClientMaintenanceError:
log.warning("Forum is in maintenance mode") log.warning("Forum is in maintenance mode")
return render_to_response('discussion/maintenance.html', {}) return render_to_response('discussion/maintenance.html', {
'disable_courseware_js': True,
'uses_pattern_library': True,
})
except ValueError: except ValueError:
return HttpResponseBadRequest("Invalid group_id") return HttpResponseBadRequest("Invalid group_id")
...@@ -266,9 +247,9 @@ def forum_form_discussion(request, course_key): ...@@ -266,9 +247,9 @@ def forum_form_discussion(request, course_key):
'course': course, 'course': course,
#'recent_active_threads': recent_active_threads, #'recent_active_threads': recent_active_threads,
'staff_access': bool(has_access(request.user, 'staff', course)), 'staff_access': bool(has_access(request.user, 'staff', course)),
'threads': json.dumps(threads), 'threads': threads,
'thread_pages': query_params['num_pages'], 'thread_pages': query_params['num_pages'],
'user_info': json.dumps(user_info, default=lambda x: None), 'user_info': user_info,
'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_comment': has_permission(request.user, "create_comment", course.id),
'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id),
...@@ -276,21 +257,21 @@ def forum_form_discussion(request, course_key): ...@@ -276,21 +257,21 @@ def forum_form_discussion(request, course_key):
has_permission(request.user, 'openclose_thread', course.id) or has_permission(request.user, 'openclose_thread', course.id) or
has_access(request.user, 'staff', course) has_access(request.user, 'staff', course)
), ),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': annotated_content_info,
'course_id': course.id.to_deprecated_string(), 'course_id': course.id.to_deprecated_string(),
'roles': json.dumps(utils.get_role_ids(course_key)), 'roles': utils.get_role_ids(course_key),
'is_moderator': has_permission(request.user, "see_all_cohorts", course_key), 'is_moderator': has_permission(request.user, "see_all_cohorts", course_key),
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template 'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
'user_cohort': user_cohort_id, # read from container in NewPostView 'user_cohort': user_cohort_id, # read from container in NewPostView
'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template 'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template
'sort_preference': user.default_sort_key, 'sort_preference': user.default_sort_key,
'category_map': course_settings["category_map"], 'category_map': course_settings["category_map"],
'course_settings': json.dumps(course_settings), 'course_settings': course_settings,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True,
} }
# print "start rendering.." # print "start rendering.."
return render_to_response('discussion/index.html', context) return render_to_response('discussion/discussion_board.html', context)
@require_GET @require_GET
...@@ -318,8 +299,8 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -318,8 +299,8 @@ def single_thread(request, course_key, discussion_id, thread_id):
response_skip=request.GET.get("resp_skip"), response_skip=request.GET.get("resp_skip"),
response_limit=request.GET.get("resp_limit") response_limit=request.GET.get("resp_limit")
) )
except cc.utils.CommentClientRequestError as e: except cc.utils.CommentClientRequestError as error:
if e.status_code == 404: if error.status_code == 404:
raise Http404 raise Http404
raise raise
...@@ -378,17 +359,17 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -378,17 +359,17 @@ def single_thread(request, course_key, discussion_id, thread_id):
'discussion_id': discussion_id, 'discussion_id': discussion_id,
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'init': '', # TODO: What is this? 'init': '', # TODO: What is this?
'user_info': json.dumps(user_info), 'user_info': user_info,
'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_comment': has_permission(request.user, "create_comment", course.id),
'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': annotated_content_info,
'course': course, 'course': course,
#'recent_active_threads': recent_active_threads, #'recent_active_threads': recent_active_threads,
'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? 'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template?
'thread_id': thread_id, 'thread_id': thread_id,
'threads': json.dumps(threads), 'threads': threads,
'roles': json.dumps(utils.get_role_ids(course_key)), 'roles': utils.get_role_ids(course_key),
'is_moderator': is_moderator, 'is_moderator': is_moderator,
'thread_pages': query_params['num_pages'], 'thread_pages': query_params['num_pages'],
'is_course_cohorted': is_course_cohorted(course_key), 'is_course_cohorted': is_course_cohorted(course_key),
...@@ -400,11 +381,11 @@ def single_thread(request, course_key, discussion_id, thread_id): ...@@ -400,11 +381,11 @@ def single_thread(request, course_key, discussion_id, thread_id):
'user_cohort': user_cohort, 'user_cohort': user_cohort,
'sort_preference': cc_user.default_sort_key, 'sort_preference': cc_user.default_sort_key,
'category_map': course_settings["category_map"], 'category_map': course_settings["category_map"],
'course_settings': json.dumps(course_settings), 'course_settings': course_settings,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True,
} }
return render_to_response('discussion/index.html', context) return render_to_response('discussion/discussion_board.html', context)
@require_GET @require_GET
...@@ -451,7 +432,7 @@ def user_profile(request, course_key, user_id): ...@@ -451,7 +432,7 @@ def user_profile(request, course_key, user_id):
'discussion_data': threads, 'discussion_data': threads,
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': annotated_content_info,
}) })
else: else:
django_user = User.objects.get(id=user_id) django_user = User.objects.get(id=user_id)
...@@ -460,15 +441,17 @@ def user_profile(request, course_key, user_id): ...@@ -460,15 +441,17 @@ def user_profile(request, course_key, user_id):
'user': request.user, 'user': request.user,
'django_user': django_user, 'django_user': django_user,
'profiled_user': profiled_user.to_dict(), 'profiled_user': profiled_user.to_dict(),
'threads': json.dumps(threads), 'threads': threads,
'user_info': json.dumps(user_info, default=lambda x: None), 'user_info': user_info,
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': annotated_content_info,
'page': query_params['page'], 'page': query_params['page'],
'num_pages': query_params['num_pages'], 'num_pages': query_params['num_pages'],
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}) 'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}),
'disable_courseware_js': True,
'uses_pattern_library': True,
} }
return render_to_response('discussion/user_profile.html', context) return render_to_response('discussion/discussion_profile_page.html', context)
except User.DoesNotExist: except User.DoesNotExist:
raise Http404 raise Http404
...@@ -547,9 +530,9 @@ def followed_threads(request, course_key, user_id): ...@@ -547,9 +530,9 @@ def followed_threads(request, course_key, user_id):
'user': request.user, 'user': request.user,
'django_user': User.objects.get(id=user_id), 'django_user': User.objects.get(id=user_id),
'profiled_user': profiled_user.to_dict(), 'profiled_user': profiled_user.to_dict(),
'threads': json.dumps(paginated_results.collection), 'threads': paginated_results.collection,
'user_info': json.dumps(user_info), 'user_info': user_info,
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': annotated_content_info,
# 'content': content, # 'content': content,
} }
......
...@@ -24,10 +24,10 @@ class GroupIdAssertionMixin(object): ...@@ -24,10 +24,10 @@ class GroupIdAssertionMixin(object):
def _assert_html_response_contains_group_info(self, response): def _assert_html_response_contains_group_info(self, response):
group_info = {"group_id": None, "group_name": None} group_info = {"group_id": None, "group_name": None}
match = re.search(r'&#34;group_id&#34;: ([\d]*)', response.content) match = re.search(r'"group_id": (\d*),', response.content)
if match and match.group(1) != '': if match and match.group(1) != '':
group_info["group_id"] = int(match.group(1)) group_info["group_id"] = int(match.group(1))
match = re.search(r'&#34;group_name&#34;: &#34;([^&]*)&#34;', response.content) match = re.search(r'"group_name": "(\w*)",', response.content)
if match: if match:
group_info["group_name"] = match.group(1) group_info["group_name"] = match.group(1)
self._assert_thread_contains_group_info(group_info) self._assert_thread_contains_group_info(group_info)
......
...@@ -6,6 +6,5 @@ from django.conf.urls import url, patterns, include ...@@ -6,6 +6,5 @@ from django.conf.urls import url, patterns, include
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'forum/?', include('django_comment_client.forum.urls')),
url(r'', include('django_comment_client.base.urls')), url(r'', include('django_comment_client.base.urls')),
) )
...@@ -591,10 +591,10 @@ def permalink(content): ...@@ -591,10 +591,10 @@ def permalink(content):
else: else:
course_id = content['course_id'] course_id = content['course_id']
if content['type'] == 'thread': if content['type'] == 'thread':
return reverse('django_comment_client.forum.views.single_thread', return reverse('discussion.views.single_thread',
args=[course_id, content['commentable_id'], content['id']]) args=[course_id, content['commentable_id'], content['id']])
else: else:
return reverse('django_comment_client.forum.views.single_thread', return reverse('discussion.views.single_thread',
args=[course_id, content['commentable_id'], content['thread_id']]) + '#' + content['id'] args=[course_id, content['commentable_id'], content['thread_id']]) + '#' + content['id']
......
define([ define([
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'teams/js/views/team_discussion', 'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/discussion_spec_helper',
'teams/js/spec_helpers/team_spec_helpers', 'teams/js/spec_helpers/team_spec_helpers',
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper' 'teams/js/views/team_discussion'
], function(_, AjaxHelpers, TeamDiscussionView, TeamSpecHelpers, DiscussionSpecHelper) { ], function(_, AjaxHelpers, DiscussionSpecHelper, TeamSpecHelpers, TeamDiscussionView) {
'use strict'; 'use strict';
xdescribe('TeamDiscussionView', function() { xdescribe('TeamDiscussionView', function() {
var discussionView, createDiscussionView, createPost, expandReplies, postReply; var discussionView, createDiscussionView, createPost, expandReplies, postReply;
...@@ -115,7 +117,7 @@ define([ ...@@ -115,7 +117,7 @@ define([
body: reply, body: reply,
comments_count: 1 comments_count: 1
}), }),
'annotated_content_info': TeamSpecHelpers.createAnnotatedContentInfo() annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
}); });
}; };
...@@ -202,8 +204,7 @@ define([ ...@@ -202,8 +204,7 @@ define([
it('cannot move a new thread to a different topic', function() { it('cannot move a new thread to a different topic', function() {
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests), view = createDiscussionView(requests);
postTopicButton;
createPost(requests, view); createPost(requests, view);
expandReplies(requests, view); expandReplies(requests, view);
view.$('.action-more .icon').first().click(); view.$('.action-more .icon').first().click();
......
define([ define([
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'teams/js/models/team', 'underscore',
'teams/js/views/team_profile', 'teams/js/spec_helpers/team_spec_helpers', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper' 'common/js/spec_helpers/discussion_spec_helper',
], function(_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) { 'teams/js/spec_helpers/team_spec_helpers',
'teams/js/models/team',
'teams/js/views/team_profile'
], function(_, AjaxHelpers, DiscussionSpecHelper, TeamSpecHelpers, TeamModel, TeamProfileView) {
'use strict'; 'use strict';
describe('TeamProfileView', function() { describe('TeamProfileView', function() {
var profileView, createTeamProfileView, createTeamModelData, clickLeaveTeam, var profileView, createTeamProfileView, createTeamModelData, clickLeaveTeam,
...@@ -10,11 +13,11 @@ define([ ...@@ -10,11 +13,11 @@ define([
leaveTeamLinkSelector = '.leave-team-link', leaveTeamLinkSelector = '.leave-team-link',
DEFAULT_MEMBERSHIP = [ DEFAULT_MEMBERSHIP = [
{ {
'user': { user: {
'username': TeamSpecHelpers.testUser, username: TeamSpecHelpers.testUser,
'profile_image': { profile_image: {
'has_image': true, has_image: true,
'image_url_medium': '/image-url' image_url_medium: '/image-url'
} }
} }
} }
...@@ -198,7 +201,7 @@ define([ ...@@ -198,7 +201,7 @@ define([
it('shows correct error messages', function() { it('shows correct error messages', function() {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
var verifyErrorMessage = function(requests, errorMessage, expectedMessage) { var verifyErrorMessage = function(errorMessage, expectedMessage) {
var view = createTeamProfileView( var view = createTeamProfileView(
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP} requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
); );
...@@ -212,22 +215,19 @@ define([ ...@@ -212,22 +215,19 @@ define([
// verify user_message // verify user_message
verifyErrorMessage( verifyErrorMessage(
requests, JSON.stringify({user_message: "can't remove user from team"}),
JSON.stringify({'user_message': "can't remove user from team"}),
"can't remove user from team" "can't remove user from team"
); );
// verify generic error message // verify generic error message
verifyErrorMessage( verifyErrorMessage(
requests,
'', '',
'An error occurred. Try again.' 'An error occurred. Try again.'
); );
// verify error message when json parsing succeeded but error message format is incorrect // verify error message when json parsing succeeded but error message format is incorrect
verifyErrorMessage( verifyErrorMessage(
requests, JSON.stringify({blah: "can't remove user from team"}),
JSON.stringify({'blah': "can't remove user from team"}),
'An error occurred. Try again.' 'An error occurred. Try again.'
); );
}); });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
*/ */
(function(define) { (function(define) {
'use strict'; 'use strict';
define(['backbone', 'underscore', 'gettext', 'DiscussionModuleView'], define(['backbone', 'underscore', 'gettext', 'common/js/discussion/discussion_module_view'],
function(Backbone, _, gettext, DiscussionModuleView) { function(Backbone, _, gettext, DiscussionModuleView) {
var TeamDiscussionView = Backbone.View.extend({ var TeamDiscussionView = Backbone.View.extend({
initialize: function() { initialize: function() {
......
...@@ -32,10 +32,6 @@ from openedx.core.djangolib.js_utils import ( ...@@ -32,10 +32,6 @@ from openedx.core.djangolib.js_utils import (
<%block name="js_extra"> <%block name="js_extra">
<%include file="../discussion/_js_body_dependencies.html" /> <%include file="../discussion/_js_body_dependencies.html" />
<%static:js group='discussion'/>
<script type="text/javascript">
RequireJS.define('DiscussionModuleView', [], function() {return window['DiscussionModuleView'];});
</script>
<%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory"> <%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory">
TeamsTabFactory({ TeamsTabFactory({
......
...@@ -1941,8 +1941,10 @@ INSTALLED_APPS = ( ...@@ -1941,8 +1941,10 @@ INSTALLED_APPS = (
'django_comment_client', 'django_comment_client',
'django_comment_common', 'django_comment_common',
'discussion_api', 'discussion_api',
'notes', 'lms.djangoapps.discussion',
# Notes
'notes',
'edxnotes', 'edxnotes',
# Splash screen # Splash screen
......
../djangoapps/discussion/static/discussion
\ No newline at end of file
...@@ -12,7 +12,7 @@ define(['jquery', 'logger', 'js/courseware/courseware_factory'], function($, Log ...@@ -12,7 +12,7 @@ define(['jquery', 'logger', 'js/courseware/courseware_factory'], function($, Log
$('.external-link').click(); $('.external-link').click();
expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', { expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', {
target_url: 'http://example.com/', target_url: 'http://example.com/',
current_url: 'http://' + window.location.host + '/context.html' current_url: window.location.toString()
}); });
}); });
...@@ -20,7 +20,7 @@ define(['jquery', 'logger', 'js/courseware/courseware_factory'], function($, Log ...@@ -20,7 +20,7 @@ define(['jquery', 'logger', 'js/courseware/courseware_factory'], function($, Log
$('.internal-link').click(); $('.internal-link').click();
expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', { expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', {
target_url: 'http://' + window.location.host + '/some/internal/link', target_url: 'http://' + window.location.host + '/some/internal/link',
current_url: 'http://' + window.location.host + '/context.html' current_url: window.location.toString()
}); });
}); });
......
...@@ -12,11 +12,11 @@ var options = { ...@@ -12,11 +12,11 @@ var options = {
// Avoid adding files to this list. Use RequireJS. // Avoid adding files to this list. Use RequireJS.
libraryFilesToInclude: [ libraryFilesToInclude: [
{pattern: 'common/js/vendor/jquery.js', included: true}, {pattern: '../../common/static/common/js/vendor/jquery.js', included: true},
{pattern: 'common/js/vendor/jquery-migrate.js', included: true}, {pattern: '../../common/static/common/js/vendor/jquery-migrate.js', included: true},
{pattern: 'xmodule_js/common_static/js/vendor/jquery.event.drag-2.2.js', included: true}, {pattern: '../../common/static/js/vendor/jquery.event.drag-2.2.js', included: true},
{pattern: 'xmodule_js/common_static/js/vendor/slick.core.js', included: true}, {pattern: '../../common/static/js/vendor/slick.core.js', included: true},
{pattern: 'xmodule_js/common_static/js/vendor/slick.grid.js', included: true} {pattern: '../../common/static/js/vendor/slick.grid.js', included: true}
], ],
libraryFiles: [ libraryFiles: [
...@@ -27,6 +27,7 @@ var options = { ...@@ -27,6 +27,7 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run. // Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [ sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'}, {pattern: 'js/**/!(*spec|djangojs).js'},
{pattern: 'lms/js/**/!(*spec).js'}, {pattern: 'lms/js/**/!(*spec).js'},
{pattern: 'support/js/**/!(*spec).js'}, {pattern: 'support/js/**/!(*spec).js'},
...@@ -34,19 +35,13 @@ var options = { ...@@ -34,19 +35,13 @@ var options = {
], ],
specFiles: [ specFiles: [
{pattern: 'js/spec/**/*spec.js'}, {pattern: '../**/*spec.js'}
{pattern: 'lms/js/spec/**/*spec.js'},
{pattern: 'support/js/spec/**/*spec.js'},
{pattern: 'teams/js/spec/**/*spec.js'},
{pattern: 'xmodule_js/common_static/coffee/spec/**/*.js'}
], ],
fixtureFiles: [ fixtureFiles: [
{pattern: 'js/fixtures/**/*.html'}, {pattern: '../**/fixtures/**/*.html'},
{pattern: 'lms/fixtures/**/*.html'}, {pattern: '../**/templates/**/*.html'},
{pattern: 'support/templates/**/*.*'}, {pattern: '../**/*.underscore'}
{pattern: 'teams/templates/**/*.*'},
{pattern: 'templates/**/*.*'}
], ],
runFiles: [ runFiles: [
......
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
* done. * done.
*/ */
modules: getModulesList([ modules: getModulesList([
'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory',
'js/api_admin/catalog_preview_factory', 'js/api_admin/catalog_preview_factory',
'js/courseware/courseware_factory', 'js/courseware/courseware_factory',
'js/discovery/discovery_factory', 'js/discovery/discovery_factory',
...@@ -76,7 +78,7 @@ ...@@ -76,7 +78,7 @@
'logger': 'empty:', 'logger': 'empty:',
'utility': 'empty:', 'utility': 'empty:',
'URI': 'empty:', 'URI': 'empty:',
'DiscussionModuleView': 'empty:', 'common/js/discussion/discussion_module_view': 'empty:',
'modernizr': 'empty', 'modernizr': 'empty',
// Don't bundle UI Toolkit helpers as they are loaded into the "edx" namespace // Don't bundle UI Toolkit helpers as they are loaded into the "edx" namespace
......
...@@ -21,12 +21,13 @@ $static-path: '../..' !default; ...@@ -21,12 +21,13 @@ $static-path: '../..' !default;
@import '../shared-v2/help-tab'; @import '../shared-v2/help-tab';
// Compatibility support for non-Pattern Library mixins and extensions // Compatibility support for non-Pattern Library mixins and extensions
@import 'utilities/v1-compatibility';
@import 'utilities/variables-v2'; @import 'utilities/variables-v2';
@import 'utilities/v1-compatibility';
// Discussion styling // Discussion styling
@import 'mixins'; @import 'mixins';
@import 'discussion'; // Process old file after definitions but before everything else, partial is deprecated. @import 'discussion'; // Process old file after definitions but before everything else, partial is deprecated.
@import 'layouts';
@import 'elements/actions'; @import 'elements/actions';
@import 'elements/editor'; @import 'elements/editor';
@import 'elements/labels'; @import 'elements/labels';
...@@ -35,5 +36,6 @@ $static-path: '../..' !default; ...@@ -35,5 +36,6 @@ $static-path: '../..' !default;
@import 'views/thread'; @import 'views/thread';
@import 'views/create-edit-post'; @import 'views/create-edit-post';
@import 'views/response'; @import 'views/response';
@import 'views/search';
@import 'utilities/developer'; @import 'utilities/developer';
@import 'utilities/shame'; @import 'utilities/shame';
// Layouts for discussion pages
.user-profile {
background-color: $sidebar-color;
.user-profile {
padding: $baseline;
min-height: 500px;
}
.sidebar-username {
font-weight: 700;
font-size: $forum-large-font-size;
}
.sidebar-user-roles {
margin-top: $baseline/2;
font-style: italic;
font-size: $forum-base-font-size;
}
.sidebar-threads-count {
margin-top: $baseline/2;
}
.sidebar-threads-count span,
.sidebar-comments-count span {
font-weight: 700;
}
}
.discussion-column {
min-height: 500px;
}
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
@mixin discussion-button() { @mixin discussion-button() {
display: block; display: block;
border: 1px solid; border: 1px solid;
border-radius: 3px; border-radius: $forum-border-radius;
height: 35px; height: 35px;
color: $white;
line-height: 35px; line-height: 35px;
font-size: 13px; font-size: $forum-base-font-size;
white-space: nowrap; // Prevent word-break in Arabic in Google Chrome white-space: nowrap; // Prevent word-break in Arabic in Google Chrome
text-shadow: none; text-shadow: none;
padding: 0 ($baseline*0.75); padding: 0 ($baseline*0.75);
...@@ -52,20 +53,20 @@ ...@@ -52,20 +53,20 @@
box-sizing: border-box; box-sizing: border-box;
margin-top: 0; margin-top: 0;
border: 1px solid $forum-color-border; border: 1px solid $forum-color-border;
border-radius: 3px 3px 0 0; border-radius: $forum-border-radius $forum-border-radius 0 0;
padding: ($baseline/2); padding: ($baseline/2);
width: 100%; width: 100%;
height: 125px; height: 125px;
background: $forum-color-background; background: $forum-color-background;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
font-size: 13px; font-size: $forum-base-font-size;
font-family: $sans-serif; font-family: $sans-serif;
line-height: 1.6; line-height: 1.6;
} }
@mixin discussion-wmd-preview-container { @mixin discussion-wmd-preview-container {
box-sizing: border-box; box-sizing: border-box;
@include border-radius(0, 0, 3px, 3px); @include border-radius(0, 0, $forum-border-radius, $forum-border-radius);
border: 1px solid $gray-l1; border: 1px solid $gray-l1;
border-top: none; border-top: none;
width: 100%; width: 100%;
...@@ -84,7 +85,7 @@ ...@@ -84,7 +85,7 @@
padding-top: 3px; padding-top: 3px;
width: 100%; width: 100%;
color: $gray-l2; color: $gray-l2;
font-size: 11px; font-size: $forum-small-font-size;
} }
@mixin discussion-wmd-preview { @mixin discussion-wmd-preview {
...@@ -111,38 +112,12 @@ ...@@ -111,38 +112,12 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@mixin forum-post-label($color) {
@extend %t-weight4;
@include font-size(9);
display: inline;
margin-top: ($baseline/4);
border: 1px solid;
border-radius: 3px;
padding: 1px 6px;
white-space: nowrap;
border-color: $color;
color: $color;
.icon {
@include margin-right($baseline/5);
}
&:last-child {
@include margin-right(0);
}
&.is-hidden {
display: none;
}
}
@mixin forum-user-label($color) { @mixin forum-user-label($color) {
@include font-size(9); @include margin-left($baseline/4);
@extend %t-weight5; @extend %t-weight5;
font-size: $forum-small-font-size;
vertical-align: middle; vertical-align: middle;
margin-left: ($baseline/4); border-radius: $forum-border-radius;
border-radius: 2px;
padding: 0 ($baseline/5); padding: 0 ($baseline/5);
background: $color; background: $color;
font-style: normal; font-style: normal;
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
box-shadow: 0 1px 1px $shadow-l1; box-shadow: 0 1px 1px $shadow-l1;
position: relative; position: relative;
width: 100%; width: 100%;
border-radius: 3px; border-radius: $forum-border-radius;
margin: ($baseline/4) 0 0 0; margin: ($baseline/4) 0 0 0;
border: 1px solid $gray-l3; border: 1px solid $gray-l3;
padding: ($baseline/2) ($baseline*0.75); padding: ($baseline/2) ($baseline*0.75);
...@@ -121,16 +121,16 @@ ...@@ -121,16 +121,16 @@
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: ($baseline/4); border-radius: $forum-border-radius;
color: $gray-l1; color: $gray-l1;
.action-icon { .action-icon {
@extend %t-icon7;
display: inline-block; display: inline-block;
font-size: $forum-small-font-size;
height: $baseline; height: $baseline;
width: $baseline; width: $baseline;
border: 1px solid $gray-l3; border: 1px solid $forum-color-border;
border-radius: 3px; border-radius: $forum-border-radius;
text-align: center; text-align: center;
color: $gray-l1; color: $gray-l1;
...@@ -156,7 +156,7 @@ ...@@ -156,7 +156,7 @@
} }
.action-icon { .action-icon {
border-radius: 0 3px 3px 0; border-radius: 0 $forum-border-radius $forum-border-radius 0;
} }
} }
...@@ -287,9 +287,9 @@ ...@@ -287,9 +287,9 @@
} }
.action-icon { .action-icon {
@include margin-left($baseline/4);
display: inline-block; display: inline-block;
width: ($baseline/2); width: ($baseline/2);
margin-left: ($baseline/4);
color: inherit; color: inherit;
} }
......
...@@ -54,9 +54,9 @@ ...@@ -54,9 +54,9 @@
.wmd-input { .wmd-input {
width: 100%; width: 100%;
height: 150px; height: 150px;
border-radius: 3px 3px 0 0; border-radius: $forum-border-radius $forum-border-radius 0 0;
font-style: normal; font-style: normal;
font-size: 0.8em; font-size: $forum-base-font-size;
font-family: Monaco, 'Lucida Console', monospace; font-family: Monaco, 'Lucida Console', monospace;
line-height: 1.6em; line-height: 1.6em;
...@@ -75,9 +75,9 @@ ...@@ -75,9 +75,9 @@
} }
.wmd-spacer { .wmd-spacer {
@include margin-left(14px);
position: absolute; position: absolute;
display: inline-block; display: inline-block;
margin-left: 14px;
width: 1px; width: 1px;
height: 20px; height: 20px;
background-color: Silver; background-color: Silver;
...@@ -129,23 +129,23 @@ ...@@ -129,23 +129,23 @@
padding: $baseline; padding: $baseline;
> div { > div {
font-size: 0.8em; font-size: $forum-base-font-size;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
} }
b { b {
font-size: 16px; font-size: $forum-large-font-size;
} }
> form > input[type="text"] { > form > input[type="text"] {
border-radius: 3px; border-radius: $forum-border-radius;
color: #333; color: #333;
} }
> form > input[type="button"] { > form > input[type="button"] {
border: 1px solid #888; border: 1px solid #888;
font-family: $sans-serif; font-family: $sans-serif;
font-size: 14px; font-size: $forum-x-large-font-size;
} }
> form > input[type="file"] { > form > input[type="file"] {
......
...@@ -2,28 +2,44 @@ ...@@ -2,28 +2,44 @@
// ==================== // ====================
body.discussion, .discussion-module { body.discussion, .discussion-module {
.post-label-pinned { .post-label {
@include forum-post-label($forum-color-pinned); @include margin($baseline/4, $baseline/2, 0, 0);
} @extend %t-weight4;
font-size: $forum-small-font-size;
display: inline;
white-space: nowrap;
.post-label-following { .icon {
@include forum-post-label($forum-color-following); @include margin-right($baseline/5);
} }
.post-label-reported { &.is-hidden {
@include forum-post-label($forum-color-reported); display: none;
} }
.post-label-closed { &.post-label-pinned {
@include forum-post-label($forum-color-closed); color: $forum-color-pinned;
} }
.post-label-by-staff { &.post-label-following {
@include forum-post-label($forum-color-staff); color: $forum-color-following;
} }
.post-label-by-community-ta { &.post-label-reported {
@include forum-post-label($forum-color-community-ta); color: $forum-color-reported;
}
&.post-label-closed {
color: $forum-color-closed;
}
&.post-label-by-staff {
color: $forum-color-staff;
}
&.post-label-by-community-ta {
color: $forum-color-community-ta;
}
} }
.user-label-staff { .user-label-staff {
...@@ -33,5 +49,15 @@ body.discussion, .discussion-module { ...@@ -33,5 +49,15 @@ body.discussion, .discussion-module {
.user-label-community-ta { .user-label-community-ta {
@include forum-user-label($forum-color-community-ta); @include forum-user-label($forum-color-community-ta);
} }
}
// Make post labels tighter when shown inside the left nav
.forum-nav-thread-link {
.forum-nav-thread-labels {
.post-label {
.icon {
@include margin-right(0);
}
}
}
} }
...@@ -44,7 +44,7 @@ body.discussion { ...@@ -44,7 +44,7 @@ body.discussion {
// alert copy // alert copy
.message { .message {
@include font-size(12); font-size: $forum-small-font-size;
color: $white; color: $white;
em { em {
...@@ -66,11 +66,11 @@ body.discussion { ...@@ -66,11 +66,11 @@ body.discussion {
text-align: right; text-align: right;
.control { .control {
@include font-size(14);
@include transition(none); @include transition(none);
@extend %t-weight5; @extend %t-weight5;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
color: $white; color: $white;
font-size: $forum-base-font-size;
// reseting poorly globally scoped hover/focus state for this control // reseting poorly globally scoped hover/focus state for this control
&:hover, &:focus { &:hover, &:focus {
......
...@@ -2,31 +2,15 @@ ...@@ -2,31 +2,15 @@
// navigation - header // navigation - header
// ------------------- // -------------------
// Override global a rules // Temporary breadcrumbs
.forum-nav-browse { .has-breadcrumbs {
color: $black !important; .breadcrumbs {
} margin: 5px 0 0 0;
// Override global label rules .all-topics .fa {
.forum-nav-search label { @include margin-right(10px);
margin-bottom: 0; }
} }
// Override global input rules
.forum-nav-search-input {
box-shadow: none !important;
border: 1px solid $forum-color-border !important;
border-radius: 3px !important;
height: auto !important;
@include padding-left($baseline/4 !important);
@include padding-right($baseline/2 + 12px !important); // Leave room for icon
font-size: 12px !important;
}
// Firefox does not compute the correct containing box for absolute positioning
// of .forum-nav-search .icon, so there's an extra div to make it happy
.forum-nav-search-ff-position-fix {
position: relative;
} }
// ------------------------ // ------------------------
...@@ -45,12 +29,12 @@ ...@@ -45,12 +29,12 @@
// Override global input rules // Override global input rules
.forum-nav-browse-filter-input { .forum-nav-browse-filter-input {
@include padding-left($baseline/4);
@include padding-right($baseline/2 + 12px); // Leave room for icon
box-shadow: none !important; box-shadow: none !important;
border-radius: 3px !important; border-radius: $forum-border-radius !important;
height: auto !important; height: auto !important;
padding-left: ($baseline/4) !important; font-size: $forum-small-font-size !important;
padding-right: ($baseline/2 + 12px) !important; // Leave room for icon
font-size: 12px !important;
} }
// Override global ul rules // Override global ul rules
...@@ -85,12 +69,6 @@ ...@@ -85,12 +69,6 @@
// The following rules would be unnecessary but for broadly scoped rules defined // The following rules would be unnecessary but for broadly scoped rules defined
// elsewhere in our CSS. // elsewhere in our CSS.
// Override global ul rules
.forum-nav-thread-list, .forum-nav-thread-labels {
margin: 0;
padding-left: 0;
}
li[class*=forum-nav-thread-label-] { li[class*=forum-nav-thread-label-] {
// Override global span rules // Override global span rules
span { span {
...@@ -160,7 +138,7 @@ li[class*=forum-nav-thread-label-] { ...@@ -160,7 +138,7 @@ li[class*=forum-nav-thread-label-] {
// Inline Discussion Module Overrides // Inline Discussion Module Overrides
// ------- // -------
.discussion-module { .discussion-module {
.wrapper-post-header .post-title { .wrapper-post-header .post-title {
margin-bottom: 0 !important; // overrides "#seq_content h1" styling margin-bottom: 0 !important; // overrides "#seq_content h1" styling
} }
......
...@@ -31,46 +31,6 @@ ...@@ -31,46 +31,6 @@
%ui-depth4 { z-index: 10000; } %ui-depth4 { z-index: 10000; }
%ui-depth5 { z-index: 100000; } %ui-depth5 { z-index: 100000; }
%t-icon1 {
@include font-size(48);
}
%t-icon2 {
@include font-size(36);
}
%t-icon3 {
@include font-size(24);
}
%t-icon4 {
@include font-size(18);
}
%t-icon5 {
@include font-size(16);
}
%t-icon6 {
@include font-size(14);
}
%t-icon7 {
@include font-size(12);
}
%t-icon8 {
@include font-size(11);
}
%t-icon9 {
@include font-size(10);
}
%t-icon-solo {
@include line-height(0);
}
// weights // weights
%t-ultrastrong { %t-ultrastrong {
font-weight: 700; font-weight: 700;
...@@ -91,97 +51,21 @@ ...@@ -91,97 +51,21 @@
font-weight: 200; font-weight: 200;
} }
// headings/titles
%t-title {
font-family: $f-sans-serif;
}
%t-title1 {
@extend %t-title;
@include font-size(60);
@include line-height(60);
}
%t-title2 {
@extend %t-title;
@include font-size(48);
@include line-height(48);
}
%t-title3 {
@include font-size(36);
@include line-height(36);
}
%t-title4 {
@extend %t-title;
@include font-size(24);
@include line-height(24);
}
%t-title5 {
@extend %t-title;
@include font-size(18);
@include line-height(18);
}
%t-title6 {
@extend %t-title;
@include font-size(16);
@include line-height(16);
}
%t-title7 {
@extend %t-title;
@include font-size(14);
@include line-height(14);
}
%t-title8 {
@extend %t-title;
@include font-size(12);
@include line-height(12);
}
%t-title9 {
@extend %t-title;
@include font-size(11);
@include line-height(11);
}
// copy // copy
%t-copy { %t-copy {
font-family: $f-sans-serif; font-family: $f-sans-serif;
} }
%t-copy-base {
@extend %t-copy;
@include font-size(16);
@include line-height(16);
}
%t-copy-lead1 {
@extend %t-copy;
@include font-size(18);
@include line-height(18);
}
%t-copy-lead2 {
@extend %t-copy;
@include font-size(24);
@include line-height(24);
}
%t-copy-sub1 { %t-copy-sub1 {
@extend %t-copy; @extend %t-copy;
@include font-size(14);
@include line-height(14); @include line-height(14);
font-size: $forum-base-font-size;
} }
%t-copy-sub2 { %t-copy-sub2 {
@extend %t-copy; @extend %t-copy;
@include font-size(12);
@include line-height(12); @include line-height(12);
font-size: $forum-small-font-size;
} }
// extends - UI - removes list styling/spacing when using uls, ols for navigation and less content-centric cases // extends - UI - removes list styling/spacing when using uls, ols for navigation and less content-centric cases
......
...@@ -14,8 +14,21 @@ $forum-color-community-ta: $green-d1 !default; ...@@ -14,8 +14,21 @@ $forum-color-community-ta: $green-d1 !default;
$forum-color-marked-answer: $green-d1 !default; $forum-color-marked-answer: $green-d1 !default;
$forum-color-border: $gray-l3 !default; $forum-color-border: $gray-l3 !default;
$forum-color-error: $red !default; $forum-color-error: $red !default;
$forum-color-hover-thread: $gray-d3 !default;
$forum-color-reading-thread: $gray-d3 !default;
$forum-color-read-post: $blue !default;
$forum-color-never-read-post: $gray-d3 !default;
// post images // post images
$post-image-dimension: ($baseline*3) !default; // image size + margin $post-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !default; // image size + margin $response-image-dimension: ($baseline*2.5) !default; // image size + margin
$comment-image-dimension: ($baseline*2) !default; // image size + margin $comment-image-dimension: ($baseline*2) !default; // image size + margin
// font sizes
$forum-base-font-size: 14px;
$forum-x-large-font-size: 21px;
$forum-large-font-size: 16px;
$forum-small-font-size: 12px;
// borders
$forum-border-radius: 3px;
...@@ -5,17 +5,30 @@ ...@@ -5,17 +5,30 @@
$forum-color-background: $lms-container-background-color !default; $forum-color-background: $lms-container-background-color !default;
$forum-color-active-thread: $lms-active-color !default; $forum-color-active-thread: $lms-active-color !default;
$forum-color-active-text: $lms-container-background-color !default; $forum-color-active-text: $lms-container-background-color !default;
$forum-color-pinned: $pink !default; $forum-color-pinned: palette(secondary, dark) !default;
$forum-color-reported: $pink !default; $forum-color-reported: palette(secondary, dark) !default;
$forum-color-closed: $black !default; $forum-color-closed: $black !default;
$forum-color-following: $blue !default; $forum-color-following: palette(primary, base) !default;
$forum-color-staff: $blue !default; $forum-color-staff: palette(primary, base) !default;
$forum-color-community-ta: $green-d1 !default; $forum-color-community-ta: palette(success, text) !default;
$forum-color-marked-answer: $green-d1 !default; $forum-color-marked-answer: palette(success, text) !default;
$forum-color-border: palette(grayscale, base) !default; $forum-color-border: palette(grayscale, back) !default;
$forum-color-error: palette(error, accent) !default; $forum-color-error: palette(error, accent) !default;
$forum-color-hover-thread: palette(grayscale, x-back) !default;
$forum-color-reading-thread: palette(primary, base) !default;
$forum-color-read-post: palette(grayscale, base) !default;
$forum-color-never-read-post: palette(primary, base) !default;
// post images // post images
$post-image-dimension: ($baseline*3) !default; // image size + margin $post-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !default; // image size + margin $response-image-dimension: ($baseline*2.5) !default; // image size + margin
$comment-image-dimension: ($baseline*2) !default; // image size + margin $comment-image-dimension: ($baseline*2) !default; // image size + margin
// font sizes
$forum-base-font-size: font-size(small);
$forum-x-large-font-size: font-size(x-large);
$forum-large-font-size: font-size(base);
$forum-small-font-size: font-size(x-small);
// borders
$forum-border-radius: $component-border-radius;
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
@include clearfix(); @include clearfix();
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
border-radius: 3px; border-radius: $forum-border-radius;
padding: $baseline; padding: $baseline;
max-width: 1180px; max-width: 1180px;
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
display: inline-block; display: inline-block;
width: 25%; width: 25%;
vertical-align: top; vertical-align: top;
font-size: 12px; font-size: $forum-small-font-size;
line-height: 40px; line-height: 40px;
} }
...@@ -47,7 +47,8 @@ ...@@ -47,7 +47,8 @@
display: inline-block; display: inline-block;
@include padding-left($baseline); @include padding-left($baseline);
width: 50%; width: 50%;
font-size: 12px; font-size: $forum-small-font-size;
} }
} }
...@@ -73,11 +74,11 @@ ...@@ -73,11 +74,11 @@
padding: 0 $baseline 0 ($baseline*0.75); padding: 0 $baseline 0 ($baseline*0.75);
width: 100%; width: 100%;
height: 40px; height: 40px;
font-size: 14px; font-size: $forum-base-font-size;
line-height: 36px; line-height: 36px;
.drop-arrow { .drop-arrow {
float: right; @include float(right);
color: #999; color: #999;
} }
} }
...@@ -89,7 +90,7 @@ ...@@ -89,7 +90,7 @@
.post-type-label { .post-type-label {
@extend %cont-truncated; @extend %cont-truncated;
@include white-button; @include white-button;
@include font-size(14); font-size: $forum-base-font-size;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
padding: 0 ($baseline/2); padding: 0 ($baseline/2);
...@@ -101,7 +102,7 @@ ...@@ -101,7 +102,7 @@
line-height: 36px; line-height: 36px;
.icon { .icon {
margin-right: ($baseline/4); @include margin-right($baseline/4);
} }
} }
...@@ -119,13 +120,13 @@ ...@@ -119,13 +120,13 @@
input[type=text].field-input { input[type=text].field-input {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid $forum-color-border; border: 1px solid $forum-color-border;
border-radius: 3px; border-radius: $forum-border-radius;
padding: 0 $baseline/2; padding: 0 $baseline/2;
height: 40px; height: 40px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
color: #333; color: #333;
font-weight: 700; font-weight: 700;
font-size: 16px; font-size: $forum-large-font-size;
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
} }
...@@ -134,7 +135,7 @@ ...@@ -134,7 +135,7 @@
display: inline-block; display: inline-block;
@include margin-right($baseline); @include margin-right($baseline);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 3px; border-radius: $forum-border-radius;
padding: ($baseline/2); padding: ($baseline/2);
&:hover { &:hover {
...@@ -147,11 +148,11 @@ ...@@ -147,11 +148,11 @@
} }
.post-option-input { .post-option-input {
margin-right: ($baseline/2); @include margin-right($baseline/2);
} }
.icon { .icon {
margin-right: 0.5em; @include margin-right($baseline/2);
} }
} }
} }
...@@ -162,8 +163,8 @@ ...@@ -162,8 +163,8 @@
.forum-new-post-form { .forum-new-post-form {
.submit { .submit {
@include blue-button; @include blue-button;
@include margin-right($baseline/2);
display: inline-block; display: inline-block;
margin-right: ($baseline/2);
} }
.cancel { .cancel {
...@@ -179,7 +180,7 @@ ...@@ -179,7 +180,7 @@
.edit-post-form { .edit-post-form {
.post-errors { .post-errors {
margin-bottom: $baseline; margin-bottom: $baseline;
border-radius: 3px; border-radius: $forum-border-radius;
padding: 0; padding: 0;
background: $error-color; background: $error-color;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2);
...@@ -230,7 +231,7 @@ ...@@ -230,7 +231,7 @@
width: 100%; width: 100%;
height: 30px; height: 30px;
color: #333; color: #333;
font-size: 11px; font-size: $forum-small-font-size;
line-height: 16px; line-height: 16px;
} }
...@@ -249,7 +250,7 @@ ...@@ -249,7 +250,7 @@
.topic-title { .topic-title {
display: block; display: block;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
font-size: 14px; font-size: $forum-base-font-size;
} }
a.topic-title { a.topic-title {
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
} }
.home-title { .home-title {
@extend %t-title5; font-size: $forum-large-font-size;
color: $black; color: $black;
margin-bottom: ($baseline/4); margin-bottom: ($baseline/4);
} }
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
vertical-align: middle; vertical-align: middle;
.count { .count {
@extend %t-title4; font-size: $forum-x-large-font-size;
display: inline-block; display: inline-block;
padding: 0 ($baseline/2); padding: 0 ($baseline/2);
vertical-align: middle; vertical-align: middle;
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
.home-helpgrid { .home-helpgrid {
border-bottom: none; border-bottom: none;
border-radius: 3px; border-radius: $forum-border-radius;
border: 1px solid $forum-color-border; border: 1px solid $forum-color-border;
box-shadow: 0 1px 3px rgba(0, 0, 0, .15); box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
} }
...@@ -84,11 +84,11 @@ ...@@ -84,11 +84,11 @@
.row-title { .row-title {
padding: ($baseline*1.5) $baseline; padding: ($baseline*1.5) $baseline;
background-color: #dedede; background-color: #dedede;
font-size: 12px; font-size: $forum-small-font-size;
} }
.row-item-full, .row-item { .row-item-full, .row-item {
font-size: 12px; font-size: $forum-small-font-size;
padding: 0 ($baseline/2); padding: 0 ($baseline/2);
width: 26%; width: 26%;
vertical-align: middle; vertical-align: middle;
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
@include margin-right($baseline/2); @include margin-right($baseline/2);
display: inline-block; display: inline-block;
@include padding($baseline/4, 0, $baseline/2, 0); @include padding($baseline/4, 0, $baseline/2, 0);
border-radius: 5px; border-radius: $forum-border-radius;
border: 1px solid gray; border: 1px solid gray;
.email-setting { .email-setting {
......
...@@ -24,14 +24,14 @@ ...@@ -24,14 +24,14 @@
position: relative; position: relative;
margin: $baseline 0; margin: $baseline 0;
border: 1px solid $forum-color-border; border: 1px solid $forum-color-border;
border-radius: 3px; border-radius: $forum-border-radius;
box-shadow: 0 0 1px $shadow; box-shadow: 0 0 1px $shadow;
} }
// wrapper - main response area // wrapper - main response area
.discussion-response { .discussion-response {
box-sizing: border-box; box-sizing: border-box;
@include border-radius(3px, 3px, 0, 0); @include border-radius($forum-border-radius, $forum-border-radius, 0, 0);
padding: $baseline; padding: $baseline;
background-color: $forum-color-background; background-color: $forum-color-background;
} }
...@@ -48,8 +48,8 @@ ...@@ -48,8 +48,8 @@
// CASE: larger username for responses // CASE: larger username for responses
.username { .username {
@include font-size(14);
@extend %t-weight5; @extend %t-weight5;
font-size: $forum-base-font-size;
} }
} }
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
// +CASE: answered question - collapsed comments in answers // +CASE: answered question - collapsed comments in answers
.forum-response .action-show-comments { .forum-response .action-show-comments {
@include font-size(13); font-size: $forum-base-font-size;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
padding: ($baseline/2) $baseline; padding: ($baseline/2) $baseline;
...@@ -95,6 +95,7 @@ ...@@ -95,6 +95,7 @@
// CASE: banner - staff response // CASE: banner - staff response
.staff-banner { .staff-banner {
@include border-radius($forum-border-radius, $forum-border-radius, 0, 0);
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
...@@ -102,9 +103,8 @@ ...@@ -102,9 +103,8 @@
height: 14px; height: 14px;
padding: 1px ($baseline/4); padding: 1px ($baseline/4);
box-sizing: border-box; box-sizing: border-box;
border-radius: 2px 2px 0 0;
background: #009fe2; background: #009fe2;
font-size: 9px; font-size: $forum-small-font-size;
font-weight: 700; font-weight: 700;
color: $white; color: $white;
} }
...@@ -118,9 +118,9 @@ ...@@ -118,9 +118,9 @@
height: 14px; height: 14px;
padding: 1px ($baseline/4); padding: 1px ($baseline/4);
box-sizing: border-box; box-sizing: border-box;
border-radius: 2px 2px 0 0; border-radius: $forum-border-radius $forum-border-radius 0 0;
background: $forum-color-community-ta; background: $forum-color-community-ta;
font-size: 9px; font-size: $forum-small-font-size;
font-weight: 700; font-weight: 700;
color: $white; color: $white;
} }
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
// +comments styling // +comments styling
.container .discussion-body .comments { .container .discussion-body .comments {
@extend %ui-no-list; @extend %ui-no-list;
border-radius: 0 0 3px 3px; border-radius: 0 0 $forum-border-radius $forum-border-radius;
background: $gray-l6; background: $gray-l6;
box-shadow: 0 1px 3px -1px $shadow inset; box-shadow: 0 1px 3px -1px $shadow inset;
...@@ -149,9 +149,9 @@ ...@@ -149,9 +149,9 @@
blockquote { blockquote {
background: $gray-l4; background: $gray-l4;
border-radius: 3px; border-radius: $forum-border-radius;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
font-size: 14px; font-size: $forum-base-font-size;
} }
.comment-form { .comment-form {
...@@ -160,12 +160,12 @@ ...@@ -160,12 +160,12 @@
.comment-form-input { .comment-form-input {
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
background-color: $forum-color-background; background-color: $forum-color-background;
font-size: 14px; font-size: $forum-base-font-size;
} }
.discussion-submit-comment { .discussion-submit-comment {
@include blue-button; @include blue-button;
float: left; @include float(left);
margin-top: 8px; margin-top: 8px;
} }
......
.forum-search {
display: inline-block;
margin-left: $baseline;
.search-input {
width: input-width(short);
}
}
...@@ -8,14 +8,15 @@ ...@@ -8,14 +8,15 @@
} }
.container { .container {
@include clearfix();
border: 1px solid $lms-border-color; border: 1px solid $lms-border-color;
background-color: $lms-container-background-color; background-color: $lms-container-background-color;
padding: $baseline;
} }
.page-header { .page-header {
@include clearfix(); @include clearfix();
border-bottom: 1px solid $lms-border-color; border-bottom: 1px solid $lms-border-color;
padding: $baseline;
.page-title { .page-title {
@extend %t-title4; @extend %t-title4;
...@@ -48,13 +49,10 @@ ...@@ -48,13 +49,10 @@
.form-actions > * { .form-actions > * {
@include margin-left($baseline/2); @include margin-left($baseline/2);
vertical-align: middle; vertical-align: middle;
min-width: 200px;
height: 34px; height: 34px;
} }
.form-actions > button { .form-actions > button {
padding: $baseline/5;
min-width: 200px;
height: 34px; height: 34px;
} }
...@@ -66,6 +64,6 @@ ...@@ -66,6 +64,6 @@
} }
.page-content { .page-content {
padding-top: $baseline; padding: $baseline;
} }
} }
// LMS variables // LMS variables
$lms-gray: palette(grayscale, base); $lms-gray: palette(grayscale, base);
$lms-background-color: rgb(252, 252, 252); // Correct shade of grey not available in the Pattern Library $lms-background-color: palette(grayscale, x-back);
$lms-container-background-color: $white; $lms-container-background-color: $white;
$lms-border-color: palette(grayscale, back); $lms-border-color: palette(grayscale, back);
$lms-label-color: palette(grayscale, black); $lms-label-color: palette(grayscale, black);
......
...@@ -52,7 +52,6 @@ ${static.get_page_title_breadcrumbs(course_name())} ...@@ -52,7 +52,6 @@ ${static.get_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:
......
...@@ -73,7 +73,8 @@ ${static.get_page_title_breadcrumbs(course_name())} ...@@ -73,7 +73,8 @@ ${static.get_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" />
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory"> <%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory">
var courseId = $('.courseware-results').data('courseId'); var courseId = $('.courseware-results').data('courseId');
...@@ -85,7 +86,6 @@ ${static.get_page_title_breadcrumbs(course_name())} ...@@ -85,7 +86,6 @@ ${static.get_page_title_breadcrumbs(course_name())}
CoursewareFactory(); CoursewareFactory();
</%static:require_module> </%static:require_module>
<%include file="../discussion/_js_body_dependencies.html" />
% if staff_access: % if staff_access:
<%include file="xqa_interface.html"/> <%include file="xqa_interface.html"/>
% endif % endif
......
<%page expression_filter="h"/>
<%inherit file="../courseware/course_navigation.html" />
<%!
from django.utils.translation import ugettext as _
from django_comment_client.permissions import has_permission
%>
<%block name="extratabs">
% if has_permission(user, 'create_thread', course.id):
<li class="right">
<button class="new-post-btn btn btn-small">${_("Add a Post")}</button>
</li>
% endif
</%block>
<%page args="disable_fast_preview=True"/> ## mako
<%namespace name='static' file='/static_content.html'/>
<%page args="disable_fast_preview=True" expression_filter="h"/>
<%!
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%include file="/mathjax_include.html" args="disable_fast_preview=disable_fast_preview"/> <%include file="/mathjax_include.html" args="disable_fast_preview=disable_fast_preview"/>
<%static:js group='discussion'/>
## Add RequireJS definitions for each discussion class
<%
discussion_classes = [
['Discussion', 'common/js/discussion/discussion'],
['DiscussionModuleView', 'common/js/discussion/discussion_module_view'],
['DiscussionThreadView', 'common/js/discussion/views/discussion_thread_view'],
['DiscussionThreadListView', 'common/js/discussion/views/discussion_thread_list_view'],
['DiscussionThreadProfileView', 'common/js/discussion/views/discussion_thread_profile_view'],
['DiscussionUtil', 'common/js/discussion/utils'],
['NewPostView', 'common/js/discussion/views/new_post_view'],
]
%>
<script type="text/javascript">
% for discussion_class in discussion_classes:
RequireJS.define(
'${discussion_class[1] | n, js_escaped_string}',
[],
function() {
return window['${discussion_class[0] | n, js_escaped_string}'];
}
);
% endfor
</script>
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<script type="text/template" id="thread-list-template"> <script type="text/template" id="thread-list-template">
<div class="forum-nav-header">
<button type="button" class="forum-nav-browse" id="forum-nav-browse" aria-haspopup="true">
## There is no whitespace between these because the front-end JS code
## needs to precisely compute the available width for forum-nav-
## browse-current in order to do truncation of topic names.
<span class="icon fa fa-reorder" aria-hidden="true"></span>
<span class="sr">${_("Discussion topics; currently listing: ")}</span>
<span class="forum-nav-browse-current">${_("All Discussions")}</span>
<span class="forum-nav-browse-drop-arrow" aria-hidden="true"></span>
</button>
<form class="forum-nav-search">
<div class="forum-nav-search-ff-position-fix">
<label>
<span class="sr">${_("Search all posts")}</span>
<input class="forum-nav-search-input" id="forum-nav-search" type="text" placeholder="${_("Search all posts")}">
<span class="icon fa fa-search" aria-hidden="true"></span>
</label>
</div>
</form>
</div>
<%include file="_filter_dropdown.html" /> <%include file="_filter_dropdown.html" />
<div class="forum-nav-thread-list-wrapper" id="sort-filter-wrapper" tabindex="-1"> <div class="forum-nav-thread-list-wrapper" id="sort-filter-wrapper" tabindex="-1">
<div class="forum-nav-refine-bar"> <div class="forum-nav-refine-bar">
......
...@@ -16,7 +16,7 @@ template_names = [ ...@@ -16,7 +16,7 @@ template_names = [
'thread', 'thread-show', 'thread-edit', 'thread-response', 'thread-response-show', 'thread-response-edit', 'thread', 'thread-show', 'thread-edit', 'thread-response', 'thread-response-show', 'thread-response-edit',
'response-comment-show', 'response-comment-edit', 'thread-list-item', 'discussion-home', 'search-alert', 'response-comment-show', 'response-comment-edit', 'thread-list-item', 'discussion-home', 'search-alert',
'new-post', 'thread-type', 'new-post-menu-entry', 'new-post-menu-category', 'topic', 'post-user-display', 'new-post', 'thread-type', 'new-post-menu-entry', 'new-post-menu-category', 'topic', 'post-user-display',
'inline-discussion', 'pagination', 'user-profile', 'profile-thread', 'customwmd-prompt', 'nav-loading' 'inline-discussion', 'pagination', 'profile-thread', 'customwmd-prompt', 'nav-loading'
] ]
## same, but without trailing "-template" in script ID - these templates does not contain any free variables ## same, but without trailing "-template" in script ID - these templates does not contain any free variables
......
## mako
<%! main_css = "style-discussion-main" %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
from django.core.urlresolvers import reverse
%>
<%block name="bodyclass">discussion</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra">
<%include file="_js_head_dependencies.html" />
</%block>
<%block name="js_extra">
## Enable fast preview to fix discussion MathJax rendering bug when page first loads.
<%include file="_js_body_dependencies.html" args="disable_fast_preview=False"/>
<%static:js group='discussion'/>
</%block>
<%include file="_discussion_course_navigation.html" args="active_page='discussion'" />
<%block name="content">
<section class="discussion container" id="discussion-container"
data-roles="${roles}"
data-course-id="${course_id}"
data-course-name="${course.display_name_with_default}"
data-user-info="${user_info}"
data-user-create-comment="${can_create_comment}"
data-user-create-subcomment="${can_create_subcomment}"
data-read-only="false"
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" role="complementary" aria-label="${_("Discussion thread list")}"></div>
<div class="discussion-column" id="discussion-column">
<main id="main" aria-label="Content" tabindex="-1">
<article class="new-post-article" style="display: none" tabindex="-1" aria-label="${_("New topic form")}"></article>
<div class="forum-content"></div>
</main>
</div>
</div>
</section>
</%block>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="../main.html" />
<h1>${_("We're sorry")}</h1>
<p>${_("The forums are currently undergoing maintenance. We'll have them back up shortly!")}</p>
<%inherit file="../main.html" />
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
%>
<%block name="bodyclass">discussion</%block>
<%block name="pagetitle">${_("Discussion - {course_number}").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra">
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<%include file="_js_head_dependencies.html" />
</%block>
<%block name="js_extra">
<%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<div class="course-wrapper">
<section class="user-profile">
<nav aria-label="${_('User Profile')}">
<article class="sidebar-module discussion-sidebar">
<%include file="_user_profile.html" />
</article>
</nav>
</section>
<section class="course-content container discussion-user-threads" data-course-id="${course.id}" data-course-name="${course.display_name_with_default}" data-threads="${threads}" data-user-info="${user_info}" data-page="${page}" data-num-pages="${num_pages}"/>
</div>
</section>
<%include file="_underscore_templates.html" />
...@@ -48,24 +48,45 @@ ...@@ -48,24 +48,45 @@
## - update the Pattern Library's markup to match ## - update the Pattern Library's markup to match
<div class="page-header-search"> <div class="page-header-search">
<form class="search-form" role="search"> <form class="search-form" role="search">
<div class="search-box"> <label class="field-label sr-only" for="search" id="search-hint">Search all the things</label>
<input class="search-field" type="text" value="" <input
aria-label="Search all the things" placeholder="Search all the things"> class="field-input input-text search-input"
<button type="button" class="btn action action-clear" aria-label="Clear search"> type="search"
<span class="fa fa-times-circle" aria-hidden="true"></span> name="search"
</button> id="search"
</div> placeholder="Search all the things"
<button type="submit" class="btn-brand action action-search" aria-label="Search items"> />
<span class="fa fa-search" aria-hidden="true"></span> <button class="btn-brand btn-small search-btn" type="button">Search</button>
</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<div class="page-content"> <div class="page-content">
<h3>This is where the page content belongs</h3> <div class="layout layout-1t2t">
<p>Useful stuff goes here</p> <aside class="layout-col layout-col-a" role="complementary" aria-label="Navigation">
<h3>Sidebar</h3>
<ul>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
</ul>
</aside>
<main id="main" aria-label="Content" tabindex="-1" class="layout-col layout-col-b">
<article tabindex="-1" aria-label="Main Content">
<h3>Main content goes here.</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis molestie, orci at viverra ornare,
augue urna fermentum ex, vitae dignissim magna est sit amet diam. Nunc sodales dolor finibus
pulvinar placerat. Suspendisse vitae tellus auctor, sodales erat ac, venenatis quam. Etiam
purus est, consequat nec erat vel, bibendum volutpat ex. Fusce vitae consectetur ante.
Suspendisse elit mauris, iaculis sed diam eu, efficitur tempor dui. Praesent tristique nunc
quam, in tincidunt ligula accumsan et. Etiam augue sem, commodo ac ipsum vel, fringilla dapibus
lacus. Sed facilisis euismod felis, non malesuada massa scelerisque sed. Etiam et placerat
lorem. Nullam quis tincidunt sapien.</p>
</article>
</main>
</div>
</div> </div>
</section> </section>
</%block> </%block>
...@@ -726,6 +726,12 @@ if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'): ...@@ -726,6 +726,12 @@ if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
include('django_comment_client.urls') include('django_comment_client.urls')
), ),
url( url(
r'^courses/{}/discussion/forum/'.format(
settings.COURSE_ID_PATTERN,
),
include('discussion.urls')
),
url(
r'^notification_prefs/enable/', r'^notification_prefs/enable/',
'notification_prefs.views.ajax_enable' 'notification_prefs.views.ajax_enable'
), ),
......
...@@ -6,7 +6,7 @@ from setuptools import setup ...@@ -6,7 +6,7 @@ from setuptools import setup
setup( setup(
name="Open edX", name="Open edX",
version="0.5", version="0.6",
install_requires=["setuptools"], install_requires=["setuptools"],
requires=[], requires=[],
# NOTE: These are not the names we should be installing. This tree should # NOTE: These are not the names we should be installing. This tree should
...@@ -23,7 +23,7 @@ setup( ...@@ -23,7 +23,7 @@ setup(
"ccx = lms.djangoapps.ccx.plugins:CcxCourseTab", "ccx = lms.djangoapps.ccx.plugins:CcxCourseTab",
"courseware = lms.djangoapps.courseware.tabs:CoursewareTab", "courseware = lms.djangoapps.courseware.tabs:CoursewareTab",
"course_info = lms.djangoapps.courseware.tabs:CourseInfoTab", "course_info = lms.djangoapps.courseware.tabs:CourseInfoTab",
"discussion = lms.djangoapps.django_comment_client.forum.views:DiscussionTab", "discussion = lms.djangoapps.discussion.plugins:DiscussionTab",
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesTab", "edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesTab",
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseTab", "external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseTab",
"external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourseTab", "external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourseTab",
......
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