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 @@
this.roleIds = roles;
};
DiscussionUtil.loadRolesFromContainer = function() {
return this.loadRoles($('#discussion-container').data('roles'));
};
DiscussionUtil.isStaff = function(userId) {
var staff;
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 @@
<th scope="row" class="row-title"><%- gettext("Find discussions") %></td>
<td class="row-item">
<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 class="row-item">
<span class="icon fa fa-search" aria-hidden="true"></span>
......
......@@ -28,6 +28,8 @@
) %>
</p>
<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>
<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">
<div class="forum-nav-thread-wrapper-0">
<%
......@@ -25,31 +25,31 @@
<% if(pinned || subscribed || staff_authored || community_ta_authored) { %>
<ul class="forum-nav-thread-labels">
<% 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>
<% // Translators: This is a label for a forum thread that has been pinned %>
<%- gettext("Pinned") %>
</li>
<% } %>
<% if (subscribed) { %>
<li class="post-label-following">
<li class="post-label post-label-following">
<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 %>
<%- gettext("Following") %>
</li>
<% } %>
<% 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>
<% // 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>
<% } %>
<% 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>
<% // Translators: This is a label for a forum thread that was authored by a community TA %>
<%- gettext("By: Community TA") %>
<%- gettext("Community TA") %>
</li>
<% } %>
</ul>
......@@ -72,7 +72,18 @@
%>
</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;
// Counts in data do not include the post itself, but the UI should
......
......@@ -38,7 +38,9 @@
<% } %>
</p>
<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 class="response-header-actions">
......
......@@ -21,9 +21,15 @@
%>
</p>
<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-reported"><span class="icon fa fa-flag" aria-hidden="true"></span><%- gettext("Reported") %></span>
<span class="post-label-closed"><span class="icon fa fa-lock" aria-hidden="true"></span><%- gettext("Closed") %></span>
<span class="post-label post-label-pinned">
<span class="icon fa fa-thumb-tack" aria-hidden="true"></span><%- gettext("Pinned") %>
</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>
<% if (!readOnly) { %>
......
......@@ -376,6 +376,10 @@ class DiscussionSortPreferencePage(CoursePage):
"""
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):
"""
Return the text of option that is selected for sorting.
......@@ -417,6 +421,10 @@ class DiscussionTabSingleThreadPage(CoursePage):
def __getattr__(self, 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):
with self.thread_page.secondary_action_menu_open(".thread-main-wrapper"):
self._find_within(".thread-main-wrapper .action-close").first.click()
......@@ -435,6 +443,7 @@ class DiscussionTabSingleThreadPage(CoursePage):
Click specific thread on the list.
"""
thread_selector = "li[data-id='{}']".format(thread_id)
self.show_all_discussions()
self.q(css=thread_selector).first.click()
EmptyPromise(
lambda: self._thread_is_rendered_successfully(thread_id),
......@@ -569,11 +578,11 @@ class DiscussionUserProfilePage(CoursePage):
def is_browser_on_page(self):
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
self.q(css='section.user-profile a.learner-profile-link').present
self.q(css='.user-profile .learner-profile-link').present
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
......@@ -670,7 +679,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
return self.q(css=".discussion-body section.home-header").present
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(
self.is_ajax_finished,
"waiting for server to return result"
......@@ -713,7 +722,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
"""
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
@property
......
......@@ -218,6 +218,65 @@ class DiscussionHomePageTest(UniqueCourseTest):
@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):
"""
Tests for the discussion page displaying a single thread
......@@ -1036,7 +1095,7 @@ class DiscussionUserProfileTest(UniqueCourseTest):
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"
def setUp(self):
......@@ -1261,6 +1320,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
self.sort_page = DiscussionSortPreferencePage(self.browser, self.course_id)
self.sort_page.visit()
self.sort_page.show_all_discussions()
def test_default_sort_preference(self):
"""
......@@ -1293,5 +1353,6 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type)
self.sort_page.refresh_page()
self.sort_page.show_all_discussions()
selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type)
......@@ -754,7 +754,7 @@ class DiscussionLinkTestCase(TabTestCase):
"""Custom reverse function"""
def reverse_discussion_link(viewname, args):
"""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 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>
<section class="discussion">
<% _.each(threads, function(thread) { %>
<article class="discussion-thread" id="thread_<%= thread.id %>"/>
<article class="discussion-thread" id="thread_<%- thread.id %>"/>
<% }); %>
</section>
<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
from lms.lib.comment_client.utils import CommentClientPaginatedResult
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.tests.group_id import (
CohortedTopicGroupIdTestMixin,
......@@ -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.utils import CohortedTestCase
from django_comment_client.utils import strip_none
from lms.djangoapps.discussion import views
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.testing import UrlResetMixin
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
......@@ -87,7 +87,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
# that gets the current user's info
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
self.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404)
......@@ -104,7 +104,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
# that gets the current user's info
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
self.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404)
......@@ -459,7 +459,7 @@ class SingleCohortedThreadTestCase(CohortedTestCase):
html = response.content
# 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)
......@@ -812,7 +812,7 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT
self.client.login(username=user.username, password='test')
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,
**headers
)
......@@ -1147,10 +1147,10 @@ class UserProfileTestCase(UrlResetMixin, ModuleStoreTestCase):
self.assertRegexpMatches(html, r'data-num-pages="1"')
self.assertRegexpMatches(html, r'<span>1</span> discussion started')
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'&#34;title&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;body&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;username&#34;: &#34;{}&#34;'.format(self.student.username))
self.assertRegexpMatches(html, r'&#39;id&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_ID))
self.assertRegexpMatches(html, r'&#39;title&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#39;body&#39;: &#39;{}&#39;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#39;username&#39;: u&#39;{}&#39;'.format(self.student.username))
def check_ajax(self, mock_request, **params):
response = self.get_response(mock_request, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
......@@ -1257,7 +1257,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
self.client.get(
reverse(
"django_comment_client.forum.views.single_thread",
"discussion.views.single_thread",
kwargs={
"course_id": self.course.id.to_deprecated_string(),
"discussion_id": "dummy_discussion_id",
......@@ -1274,7 +1274,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
self.client.get(
reverse(
"django_comment_client.forum.views.forum_form_discussion",
"discussion.views.forum_form_discussion",
kwargs={"course_id": self.course.id.to_deprecated_string()}
),
)
......@@ -1360,8 +1360,9 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase):
"""
Test that XSS attack is prevented
"""
mock_user.return_value.to_dict.return_value = {}
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')
# Test that malicious code does not appear in html
url = "%s?%s=%s" % (reverse_url, 'sort_key', malicious_code)
......@@ -1377,10 +1378,10 @@ class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase):
Test that XSS attack is prevented
"""
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')
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)})
# Test that malicious code does not appear in html
url_string = "%s?%s=%s" % (url, 'page', malicious_code)
......
......@@ -4,7 +4,7 @@ Forum urls for the django_comment_client.
from django.conf.urls import url, 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+)$', 'user_profile', name='user_profile'),
......
......@@ -3,27 +3,23 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
"""
from functools import wraps
import json
import logging
from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
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
import newrelic.agent
from edxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access
from openedx.core.djangoapps.course_groups.cohorts import (
is_course_cohorted,
get_cohort_id,
get_course_cohorts,
)
from courseware.tabs import EnrolledTab
from courseware.access import has_access
from xmodule.modulestore.django import modulestore
......@@ -48,25 +44,6 @@ PAGES_NEARBY_DELTA = 2
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()
def make_course_settings(course, user):
"""
......@@ -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
cc_user = cc.User.from_django_user(request.user)
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']
else:
# If the user clicked a sort key, update their default sort 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]
except cc.utils.CommentClientMaintenanceError:
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:
return HttpResponseBadRequest("Invalid group_id")
......@@ -266,9 +247,9 @@ def forum_form_discussion(request, course_key):
'course': course,
#'recent_active_threads': recent_active_threads,
'staff_access': bool(has_access(request.user, 'staff', course)),
'threads': json.dumps(threads),
'threads': threads,
'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_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id),
......@@ -276,21 +257,21 @@ def forum_form_discussion(request, course_key):
has_permission(request.user, 'openclose_thread', course.id) or
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(),
'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),
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
'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
'sort_preference': user.default_sort_key,
'category_map': course_settings["category_map"],
'course_settings': json.dumps(course_settings),
'course_settings': course_settings,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
# print "start rendering.."
return render_to_response('discussion/index.html', context)
return render_to_response('discussion/discussion_board.html', context)
@require_GET
......@@ -318,8 +299,8 @@ def single_thread(request, course_key, discussion_id, thread_id):
response_skip=request.GET.get("resp_skip"),
response_limit=request.GET.get("resp_limit")
)
except cc.utils.CommentClientRequestError as e:
if e.status_code == 404:
except cc.utils.CommentClientRequestError as error:
if error.status_code == 404:
raise Http404
raise
......@@ -378,17 +359,17 @@ def single_thread(request, course_key, discussion_id, thread_id):
'discussion_id': discussion_id,
'csrf': csrf(request)['csrf_token'],
'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_subcomment': has_permission(request.user, "create_sub_comment", course.id),
'can_create_thread': has_permission(request.user, "create_thread", course.id),
'annotated_content_info': json.dumps(annotated_content_info),
'annotated_content_info': annotated_content_info,
'course': course,
#'recent_active_threads': recent_active_threads,
'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template?
'thread_id': thread_id,
'threads': json.dumps(threads),
'roles': json.dumps(utils.get_role_ids(course_key)),
'threads': threads,
'roles': utils.get_role_ids(course_key),
'is_moderator': is_moderator,
'thread_pages': query_params['num_pages'],
'is_course_cohorted': is_course_cohorted(course_key),
......@@ -400,11 +381,11 @@ def single_thread(request, course_key, discussion_id, thread_id):
'user_cohort': user_cohort,
'sort_preference': cc_user.default_sort_key,
'category_map': course_settings["category_map"],
'course_settings': json.dumps(course_settings),
'course_settings': course_settings,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
return render_to_response('discussion/index.html', context)
return render_to_response('discussion/discussion_board.html', context)
@require_GET
......@@ -451,7 +432,7 @@ def user_profile(request, course_key, user_id):
'discussion_data': threads,
'page': query_params['page'],
'num_pages': query_params['num_pages'],
'annotated_content_info': json.dumps(annotated_content_info),
'annotated_content_info': annotated_content_info,
})
else:
django_user = User.objects.get(id=user_id)
......@@ -460,15 +441,17 @@ def user_profile(request, course_key, user_id):
'user': request.user,
'django_user': django_user,
'profiled_user': profiled_user.to_dict(),
'threads': json.dumps(threads),
'user_info': json.dumps(user_info, default=lambda x: None),
'annotated_content_info': json.dumps(annotated_content_info),
'threads': threads,
'user_info': user_info,
'annotated_content_info': annotated_content_info,
'page': query_params['page'],
'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:
raise Http404
......@@ -547,9 +530,9 @@ def followed_threads(request, course_key, user_id):
'user': request.user,
'django_user': User.objects.get(id=user_id),
'profiled_user': profiled_user.to_dict(),
'threads': json.dumps(paginated_results.collection),
'user_info': json.dumps(user_info),
'annotated_content_info': json.dumps(annotated_content_info),
'threads': paginated_results.collection,
'user_info': user_info,
'annotated_content_info': annotated_content_info,
# 'content': content,
}
......
......@@ -24,10 +24,10 @@ class GroupIdAssertionMixin(object):
def _assert_html_response_contains_group_info(self, response):
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) != '':
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:
group_info["group_name"] = match.group(1)
self._assert_thread_contains_group_info(group_info)
......
......@@ -6,6 +6,5 @@ from django.conf.urls import url, patterns, include
urlpatterns = patterns(
'',
url(r'forum/?', include('django_comment_client.forum.urls')),
url(r'', include('django_comment_client.base.urls')),
)
......@@ -591,10 +591,10 @@ def permalink(content):
else:
course_id = content['course_id']
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']])
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']
......
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',
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper'
], function(_, AjaxHelpers, TeamDiscussionView, TeamSpecHelpers, DiscussionSpecHelper) {
'teams/js/views/team_discussion'
], function(_, AjaxHelpers, DiscussionSpecHelper, TeamSpecHelpers, TeamDiscussionView) {
'use strict';
xdescribe('TeamDiscussionView', function() {
var discussionView, createDiscussionView, createPost, expandReplies, postReply;
......@@ -115,7 +117,7 @@ define([
body: reply,
comments_count: 1
}),
'annotated_content_info': TeamSpecHelpers.createAnnotatedContentInfo()
annotated_content_info: TeamSpecHelpers.createAnnotatedContentInfo()
});
};
......@@ -202,8 +204,7 @@ define([
it('cannot move a new thread to a different topic', function() {
var requests = AjaxHelpers.requests(this),
view = createDiscussionView(requests),
postTopicButton;
view = createDiscussionView(requests);
createPost(requests, view);
expandReplies(requests, view);
view.$('.action-more .icon').first().click();
......
define([
'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'teams/js/models/team',
'teams/js/views/team_profile', 'teams/js/spec_helpers/team_spec_helpers',
'xmodule_js/common_static/common/js/spec_helpers/discussion_spec_helper'
], function(_, AjaxHelpers, TeamModel, TeamProfileView, TeamSpecHelpers, DiscussionSpecHelper) {
'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/models/team',
'teams/js/views/team_profile'
], function(_, AjaxHelpers, DiscussionSpecHelper, TeamSpecHelpers, TeamModel, TeamProfileView) {
'use strict';
describe('TeamProfileView', function() {
var profileView, createTeamProfileView, createTeamModelData, clickLeaveTeam,
......@@ -10,11 +13,11 @@ define([
leaveTeamLinkSelector = '.leave-team-link',
DEFAULT_MEMBERSHIP = [
{
'user': {
'username': TeamSpecHelpers.testUser,
'profile_image': {
'has_image': true,
'image_url_medium': '/image-url'
user: {
username: TeamSpecHelpers.testUser,
profile_image: {
has_image: true,
image_url_medium: '/image-url'
}
}
}
......@@ -198,7 +201,7 @@ define([
it('shows correct error messages', function() {
var requests = AjaxHelpers.requests(this);
var verifyErrorMessage = function(requests, errorMessage, expectedMessage) {
var verifyErrorMessage = function(errorMessage, expectedMessage) {
var view = createTeamProfileView(
requests, {country: 'US', language: 'en', membership: DEFAULT_MEMBERSHIP}
);
......@@ -212,22 +215,19 @@ define([
// verify user_message
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"
);
// verify generic error message
verifyErrorMessage(
requests,
'',
'An error occurred. Try again.'
);
// verify error message when json parsing succeeded but error message format is incorrect
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.'
);
});
......
......@@ -3,7 +3,7 @@
*/
(function(define) {
'use strict';
define(['backbone', 'underscore', 'gettext', 'DiscussionModuleView'],
define(['backbone', 'underscore', 'gettext', 'common/js/discussion/discussion_module_view'],
function(Backbone, _, gettext, DiscussionModuleView) {
var TeamDiscussionView = Backbone.View.extend({
initialize: function() {
......
......@@ -32,10 +32,6 @@ from openedx.core.djangolib.js_utils import (
<%block name="js_extra">
<%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">
TeamsTabFactory({
......
......@@ -1941,8 +1941,10 @@ INSTALLED_APPS = (
'django_comment_client',
'django_comment_common',
'discussion_api',
'notes',
'lms.djangoapps.discussion',
# Notes
'notes',
'edxnotes',
# 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
$('.external-link').click();
expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', {
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
$('.internal-link').click();
expect(Logger.log).toHaveBeenCalledWith('edx.ui.lms.link_clicked', {
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 = {
// Avoid adding files to this list. Use RequireJS.
libraryFilesToInclude: [
{pattern: 'common/js/vendor/jquery.js', included: true},
{pattern: 'common/js/vendor/jquery-migrate.js', included: true},
{pattern: 'xmodule_js/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: 'xmodule_js/common_static/js/vendor/slick.grid.js', included: true}
{pattern: '../../common/static/common/js/vendor/jquery.js', included: true},
{pattern: '../../common/static/common/js/vendor/jquery-migrate.js', included: true},
{pattern: '../../common/static/js/vendor/jquery.event.drag-2.2.js', included: true},
{pattern: '../../common/static/js/vendor/slick.core.js', included: true},
{pattern: '../../common/static/js/vendor/slick.grid.js', included: true}
],
libraryFiles: [
......@@ -27,6 +27,7 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'},
{pattern: 'lms/js/**/!(*spec).js'},
{pattern: 'support/js/**/!(*spec).js'},
......@@ -34,19 +35,13 @@ var options = {
],
specFiles: [
{pattern: 'js/spec/**/*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'}
{pattern: '../**/*spec.js'}
],
fixtureFiles: [
{pattern: 'js/fixtures/**/*.html'},
{pattern: 'lms/fixtures/**/*.html'},
{pattern: 'support/templates/**/*.*'},
{pattern: 'teams/templates/**/*.*'},
{pattern: 'templates/**/*.*'}
{pattern: '../**/fixtures/**/*.html'},
{pattern: '../**/templates/**/*.html'},
{pattern: '../**/*.underscore'}
],
runFiles: [
......
......@@ -18,6 +18,8 @@
* done.
*/
modules: getModulesList([
'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory',
'js/api_admin/catalog_preview_factory',
'js/courseware/courseware_factory',
'js/discovery/discovery_factory',
......@@ -76,7 +78,7 @@
'logger': 'empty:',
'utility': 'empty:',
'URI': 'empty:',
'DiscussionModuleView': 'empty:',
'common/js/discussion/discussion_module_view': 'empty:',
'modernizr': 'empty',
// Don't bundle UI Toolkit helpers as they are loaded into the "edx" namespace
......
......@@ -21,12 +21,13 @@ $static-path: '../..' !default;
@import '../shared-v2/help-tab';
// Compatibility support for non-Pattern Library mixins and extensions
@import 'utilities/v1-compatibility';
@import 'utilities/variables-v2';
@import 'utilities/v1-compatibility';
// Discussion styling
@import 'mixins';
@import 'discussion'; // Process old file after definitions but before everything else, partial is deprecated.
@import 'layouts';
@import 'elements/actions';
@import 'elements/editor';
@import 'elements/labels';
......@@ -35,5 +36,6 @@ $static-path: '../..' !default;
@import 'views/thread';
@import 'views/create-edit-post';
@import 'views/response';
@import 'views/search';
@import 'utilities/developer';
@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 @@
@mixin discussion-button() {
display: block;
border: 1px solid;
border-radius: 3px;
border-radius: $forum-border-radius;
height: 35px;
color: $white;
line-height: 35px;
font-size: 13px;
font-size: $forum-base-font-size;
white-space: nowrap; // Prevent word-break in Arabic in Google Chrome
text-shadow: none;
padding: 0 ($baseline*0.75);
......@@ -52,20 +53,20 @@
box-sizing: border-box;
margin-top: 0;
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);
width: 100%;
height: 125px;
background: $forum-color-background;
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;
line-height: 1.6;
}
@mixin discussion-wmd-preview-container {
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-top: none;
width: 100%;
......@@ -84,7 +85,7 @@
padding-top: 3px;
width: 100%;
color: $gray-l2;
font-size: 11px;
font-size: $forum-small-font-size;
}
@mixin discussion-wmd-preview {
......@@ -111,38 +112,12 @@
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) {
@include font-size(9);
@include margin-left($baseline/4);
@extend %t-weight5;
font-size: $forum-small-font-size;
vertical-align: middle;
margin-left: ($baseline/4);
border-radius: 2px;
border-radius: $forum-border-radius;
padding: 0 ($baseline/5);
background: $color;
font-style: normal;
......
......@@ -70,7 +70,7 @@
box-shadow: 0 1px 1px $shadow-l1;
position: relative;
width: 100%;
border-radius: 3px;
border-radius: $forum-border-radius;
margin: ($baseline/4) 0 0 0;
border: 1px solid $gray-l3;
padding: ($baseline/2) ($baseline*0.75);
......@@ -121,16 +121,16 @@
box-sizing: border-box;
display: inline-block;
border: 1px solid transparent;
border-radius: ($baseline/4);
border-radius: $forum-border-radius;
color: $gray-l1;
.action-icon {
@extend %t-icon7;
display: inline-block;
font-size: $forum-small-font-size;
height: $baseline;
width: $baseline;
border: 1px solid $gray-l3;
border-radius: 3px;
border: 1px solid $forum-color-border;
border-radius: $forum-border-radius;
text-align: center;
color: $gray-l1;
......@@ -156,7 +156,7 @@
}
.action-icon {
border-radius: 0 3px 3px 0;
border-radius: 0 $forum-border-radius $forum-border-radius 0;
}
}
......@@ -287,9 +287,9 @@
}
.action-icon {
@include margin-left($baseline/4);
display: inline-block;
width: ($baseline/2);
margin-left: ($baseline/4);
color: inherit;
}
......
......@@ -54,9 +54,9 @@
.wmd-input {
width: 100%;
height: 150px;
border-radius: 3px 3px 0 0;
border-radius: $forum-border-radius $forum-border-radius 0 0;
font-style: normal;
font-size: 0.8em;
font-size: $forum-base-font-size;
font-family: Monaco, 'Lucida Console', monospace;
line-height: 1.6em;
......@@ -75,9 +75,9 @@
}
.wmd-spacer {
@include margin-left(14px);
position: absolute;
display: inline-block;
margin-left: 14px;
width: 1px;
height: 20px;
background-color: Silver;
......@@ -129,23 +129,23 @@
padding: $baseline;
> div {
font-size: 0.8em;
font-size: $forum-base-font-size;
font-family: arial, helvetica, sans-serif;
}
b {
font-size: 16px;
font-size: $forum-large-font-size;
}
> form > input[type="text"] {
border-radius: 3px;
border-radius: $forum-border-radius;
color: #333;
}
> form > input[type="button"] {
border: 1px solid #888;
font-family: $sans-serif;
font-size: 14px;
font-size: $forum-x-large-font-size;
}
> form > input[type="file"] {
......
......@@ -2,28 +2,44 @@
// ====================
body.discussion, .discussion-module {
.post-label-pinned {
@include forum-post-label($forum-color-pinned);
}
.post-label {
@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 {
@include forum-post-label($forum-color-following);
}
.icon {
@include margin-right($baseline/5);
}
.post-label-reported {
@include forum-post-label($forum-color-reported);
}
&.is-hidden {
display: none;
}
.post-label-closed {
@include forum-post-label($forum-color-closed);
}
&.post-label-pinned {
color: $forum-color-pinned;
}
.post-label-by-staff {
@include forum-post-label($forum-color-staff);
}
&.post-label-following {
color: $forum-color-following;
}
.post-label-by-community-ta {
@include forum-post-label($forum-color-community-ta);
&.post-label-reported {
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 {
......@@ -33,5 +49,15 @@ body.discussion, .discussion-module {
.user-label-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 {
// alert copy
.message {
@include font-size(12);
font-size: $forum-small-font-size;
color: $white;
em {
......@@ -66,11 +66,11 @@ body.discussion {
text-align: right;
.control {
@include font-size(14);
@include transition(none);
@extend %t-weight5;
padding: ($baseline/4) ($baseline/2);
color: $white;
font-size: $forum-base-font-size;
// reseting poorly globally scoped hover/focus state for this control
&:hover, &:focus {
......
......@@ -2,31 +2,15 @@
// navigation - header
// -------------------
// Override global a rules
.forum-nav-browse {
color: $black !important;
}
// Override global label rules
.forum-nav-search label {
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;
// Temporary breadcrumbs
.has-breadcrumbs {
.breadcrumbs {
margin: 5px 0 0 0;
.all-topics .fa {
@include margin-right(10px);
}
}
}
// ------------------------
......@@ -45,12 +29,12 @@
// Override global input rules
.forum-nav-browse-filter-input {
@include padding-left($baseline/4);
@include padding-right($baseline/2 + 12px); // Leave room for icon
box-shadow: none !important;
border-radius: 3px !important;
border-radius: $forum-border-radius !important;
height: auto !important;
padding-left: ($baseline/4) !important;
padding-right: ($baseline/2 + 12px) !important; // Leave room for icon
font-size: 12px !important;
font-size: $forum-small-font-size !important;
}
// Override global ul rules
......@@ -85,12 +69,6 @@
// The following rules would be unnecessary but for broadly scoped rules defined
// 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-] {
// Override global span rules
span {
......@@ -160,7 +138,7 @@ li[class*=forum-nav-thread-label-] {
// Inline Discussion Module Overrides
// -------
.discussion-module {
.wrapper-post-header .post-title {
margin-bottom: 0 !important; // overrides "#seq_content h1" styling
}
......
......@@ -31,46 +31,6 @@
%ui-depth4 { z-index: 10000; }
%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
%t-ultrastrong {
font-weight: 700;
......@@ -91,97 +51,21 @@
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
%t-copy {
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 {
@extend %t-copy;
@include font-size(14);
@include line-height(14);
font-size: $forum-base-font-size;
}
%t-copy-sub2 {
@extend %t-copy;
@include font-size(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
......
......@@ -14,8 +14,21 @@ $forum-color-community-ta: $green-d1 !default;
$forum-color-marked-answer: $green-d1 !default;
$forum-color-border: $gray-l3 !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-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !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 @@
$forum-color-background: $lms-container-background-color !default;
$forum-color-active-thread: $lms-active-color !default;
$forum-color-active-text: $lms-container-background-color !default;
$forum-color-pinned: $pink !default;
$forum-color-reported: $pink !default;
$forum-color-pinned: palette(secondary, dark) !default;
$forum-color-reported: palette(secondary, dark) !default;
$forum-color-closed: $black !default;
$forum-color-following: $blue !default;
$forum-color-staff: $blue !default;
$forum-color-community-ta: $green-d1 !default;
$forum-color-marked-answer: $green-d1 !default;
$forum-color-border: palette(grayscale, base) !default;
$forum-color-following: palette(primary, base) !default;
$forum-color-staff: palette(primary, base) !default;
$forum-color-community-ta: palette(success, text) !default;
$forum-color-marked-answer: palette(success, text) !default;
$forum-color-border: palette(grayscale, back) !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-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !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 @@
@include clearfix();
box-sizing: border-box;
margin: 0;
border-radius: 3px;
border-radius: $forum-border-radius;
padding: $baseline;
max-width: 1180px;
......@@ -32,7 +32,7 @@
display: inline-block;
width: 25%;
vertical-align: top;
font-size: 12px;
font-size: $forum-small-font-size;
line-height: 40px;
}
......@@ -47,7 +47,8 @@
display: inline-block;
@include padding-left($baseline);
width: 50%;
font-size: 12px;
font-size: $forum-small-font-size;
}
}
......@@ -73,11 +74,11 @@
padding: 0 $baseline 0 ($baseline*0.75);
width: 100%;
height: 40px;
font-size: 14px;
font-size: $forum-base-font-size;
line-height: 36px;
.drop-arrow {
float: right;
@include float(right);
color: #999;
}
}
......@@ -89,7 +90,7 @@
.post-type-label {
@extend %cont-truncated;
@include white-button;
@include font-size(14);
font-size: $forum-base-font-size;
box-sizing: border-box;
display: inline-block;
padding: 0 ($baseline/2);
......@@ -101,7 +102,7 @@
line-height: 36px;
.icon {
margin-right: ($baseline/4);
@include margin-right($baseline/4);
}
}
......@@ -119,13 +120,13 @@
input[type=text].field-input {
box-sizing: border-box;
border: 1px solid $forum-color-border;
border-radius: 3px;
border-radius: $forum-border-radius;
padding: 0 $baseline/2;
height: 40px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
color: #333;
font-weight: 700;
font-size: 16px;
font-size: $forum-large-font-size;
font-family: 'Open Sans', sans-serif;
}
......@@ -134,7 +135,7 @@
display: inline-block;
@include margin-right($baseline);
border: 1px solid transparent;
border-radius: 3px;
border-radius: $forum-border-radius;
padding: ($baseline/2);
&:hover {
......@@ -147,11 +148,11 @@
}
.post-option-input {
margin-right: ($baseline/2);
@include margin-right($baseline/2);
}
.icon {
margin-right: 0.5em;
@include margin-right($baseline/2);
}
}
}
......@@ -162,8 +163,8 @@
.forum-new-post-form {
.submit {
@include blue-button;
@include margin-right($baseline/2);
display: inline-block;
margin-right: ($baseline/2);
}
.cancel {
......@@ -179,7 +180,7 @@
.edit-post-form {
.post-errors {
margin-bottom: $baseline;
border-radius: 3px;
border-radius: $forum-border-radius;
padding: 0;
background: $error-color;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2);
......@@ -230,7 +231,7 @@
width: 100%;
height: 30px;
color: #333;
font-size: 11px;
font-size: $forum-small-font-size;
line-height: 16px;
}
......@@ -249,7 +250,7 @@
.topic-title {
display: block;
padding: ($baseline/4) ($baseline/2);
font-size: 14px;
font-size: $forum-base-font-size;
}
a.topic-title {
......
......@@ -24,7 +24,7 @@
}
.home-title {
@extend %t-title5;
font-size: $forum-large-font-size;
color: $black;
margin-bottom: ($baseline/4);
}
......@@ -58,7 +58,7 @@
vertical-align: middle;
.count {
@extend %t-title4;
font-size: $forum-x-large-font-size;
display: inline-block;
padding: 0 ($baseline/2);
vertical-align: middle;
......@@ -73,7 +73,7 @@
.home-helpgrid {
border-bottom: none;
border-radius: 3px;
border-radius: $forum-border-radius;
border: 1px solid $forum-color-border;
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
}
......@@ -84,11 +84,11 @@
.row-title {
padding: ($baseline*1.5) $baseline;
background-color: #dedede;
font-size: 12px;
font-size: $forum-small-font-size;
}
.row-item-full, .row-item {
font-size: 12px;
font-size: $forum-small-font-size;
padding: 0 ($baseline/2);
width: 26%;
vertical-align: middle;
......@@ -118,7 +118,7 @@
@include margin-right($baseline/2);
display: inline-block;
@include padding($baseline/4, 0, $baseline/2, 0);
border-radius: 5px;
border-radius: $forum-border-radius;
border: 1px solid gray;
.email-setting {
......
......@@ -24,14 +24,14 @@
position: relative;
margin: $baseline 0;
border: 1px solid $forum-color-border;
border-radius: 3px;
border-radius: $forum-border-radius;
box-shadow: 0 0 1px $shadow;
}
// wrapper - main response area
.discussion-response {
box-sizing: border-box;
@include border-radius(3px, 3px, 0, 0);
@include border-radius($forum-border-radius, $forum-border-radius, 0, 0);
padding: $baseline;
background-color: $forum-color-background;
}
......@@ -48,8 +48,8 @@
// CASE: larger username for responses
.username {
@include font-size(14);
@extend %t-weight5;
font-size: $forum-base-font-size;
}
}
......@@ -68,7 +68,7 @@
// +CASE: answered question - collapsed comments in answers
.forum-response .action-show-comments {
@include font-size(13);
font-size: $forum-base-font-size;
box-sizing: border-box;
display: block;
padding: ($baseline/2) $baseline;
......@@ -95,6 +95,7 @@
// CASE: banner - staff response
.staff-banner {
@include border-radius($forum-border-radius, $forum-border-radius, 0, 0);
position: absolute;
top: 0;
left: 0;
......@@ -102,9 +103,8 @@
height: 14px;
padding: 1px ($baseline/4);
box-sizing: border-box;
border-radius: 2px 2px 0 0;
background: #009fe2;
font-size: 9px;
font-size: $forum-small-font-size;
font-weight: 700;
color: $white;
}
......@@ -118,9 +118,9 @@
height: 14px;
padding: 1px ($baseline/4);
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;
font-size: 9px;
font-size: $forum-small-font-size;
font-weight: 700;
color: $white;
}
......@@ -138,7 +138,7 @@
// +comments styling
.container .discussion-body .comments {
@extend %ui-no-list;
border-radius: 0 0 3px 3px;
border-radius: 0 0 $forum-border-radius $forum-border-radius;
background: $gray-l6;
box-shadow: 0 1px 3px -1px $shadow inset;
......@@ -149,9 +149,9 @@
blockquote {
background: $gray-l4;
border-radius: 3px;
border-radius: $forum-border-radius;
padding: ($baseline/4) ($baseline/2);
font-size: 14px;
font-size: $forum-base-font-size;
}
.comment-form {
......@@ -160,12 +160,12 @@
.comment-form-input {
padding: ($baseline/4) ($baseline/2);
background-color: $forum-color-background;
font-size: 14px;
font-size: $forum-base-font-size;
}
.discussion-submit-comment {
@include blue-button;
float: left;
@include float(left);
margin-top: 8px;
}
......
.forum-search {
display: inline-block;
margin-left: $baseline;
.search-input {
width: input-width(short);
}
}
......@@ -8,14 +8,15 @@
}
.container {
@include clearfix();
border: 1px solid $lms-border-color;
background-color: $lms-container-background-color;
padding: $baseline;
}
.page-header {
@include clearfix();
border-bottom: 1px solid $lms-border-color;
padding: $baseline;
.page-title {
@extend %t-title4;
......@@ -48,13 +49,10 @@
.form-actions > * {
@include margin-left($baseline/2);
vertical-align: middle;
min-width: 200px;
height: 34px;
}
.form-actions > button {
padding: $baseline/5;
min-width: 200px;
height: 34px;
}
......@@ -66,6 +64,6 @@
}
.page-content {
padding-top: $baseline;
padding: $baseline;
}
}
// LMS variables
$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-border-color: palette(grayscale, back);
$lms-label-color: palette(grayscale, black);
......
......@@ -52,7 +52,6 @@ ${static.get_page_title_breadcrumbs(course_name())}
<script type="text/javascript" src="${static.url('js/vendor/codemirror-compressed.js')}"></script>
<%static:js group='courseware'/>
<%static:js group='discussion'/>
<%include file="../discussion/_js_body_dependencies.html" />
% if staff_access:
......
......@@ -73,7 +73,8 @@ ${static.get_page_title_breadcrumbs(course_name())}
<script type="text/javascript" src="${static.url('js/vendor/codemirror-compressed.js')}"></script>
<%static:js group='courseware'/>
<%static:js group='discussion'/>
<%include file="../discussion/_js_body_dependencies.html" />
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory">
var courseId = $('.courseware-results').data('courseId');
......@@ -85,7 +86,6 @@ ${static.get_page_title_breadcrumbs(course_name())}
CoursewareFactory();
</%static:require_module>
<%include file="../discussion/_js_body_dependencies.html" />
% if staff_access:
<%include file="xqa_interface.html"/>
% 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"/>
<%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"/>
<%! from django.utils.translation import ugettext as _ %>
<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" />
<div class="forum-nav-thread-list-wrapper" id="sort-filter-wrapper" tabindex="-1">
<div class="forum-nav-refine-bar">
......
......@@ -16,7 +16,7 @@ template_names = [
'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',
'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
......
## 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 @@
## - update the Pattern Library's markup to match
<div class="page-header-search">
<form class="search-form" role="search">
<div class="search-box">
<input class="search-field" type="text" value=""
aria-label="Search all the things" placeholder="Search all the things">
<button type="button" class="btn action action-clear" aria-label="Clear search">
<span class="fa fa-times-circle" aria-hidden="true"></span>
</button>
</div>
<button type="submit" class="btn-brand action action-search" aria-label="Search items">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
<label class="field-label sr-only" for="search" id="search-hint">Search all the things</label>
<input
class="field-input input-text search-input"
type="search"
name="search"
id="search"
placeholder="Search all the things"
/>
<button class="btn-brand btn-small search-btn" type="button">Search</button>
</form>
</div>
</div>
</div>
</header>
<div class="page-content">
<h3>This is where the page content belongs</h3>
<p>Useful stuff goes here</p>
<div class="layout layout-1t2t">
<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>
</section>
</%block>
......@@ -726,6 +726,12 @@ if settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
include('django_comment_client.urls')
),
url(
r'^courses/{}/discussion/forum/'.format(
settings.COURSE_ID_PATTERN,
),
include('discussion.urls')
),
url(
r'^notification_prefs/enable/',
'notification_prefs.views.ajax_enable'
),
......
......@@ -6,7 +6,7 @@ from setuptools import setup
setup(
name="Open edX",
version="0.5",
version="0.6",
install_requires=["setuptools"],
requires=[],
# NOTE: These are not the names we should be installing. This tree should
......@@ -23,7 +23,7 @@ setup(
"ccx = lms.djangoapps.ccx.plugins:CcxCourseTab",
"courseware = lms.djangoapps.courseware.tabs:CoursewareTab",
"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",
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseTab",
"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