Commit 988e4e6d by Greg Price

Update UI for forum actions

The actions are now consolidated in one location for each piece of
content. Primary actions (vote, follow, endorse, mark as answer) are
buttons, and secondary actions (pin, edit, delete, report, close) are in
a menu. This also includes improved front-end error handling for the
actions and significant test cleanup.

Co-authored-by: jsa <jsa@edx.org>
Co-authored-by: marco <marcotuts@gmail.com>
Co-authored-by: Frances Botsford <frances@edx.org>
Co-authored-by: Brian Talbot <btalbot@edx.org>
parent a99196a1
......@@ -4,3 +4,605 @@ class @DiscussionSpecHelper
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
window.$$course_id = "edX/999/test"
window.user = new DiscussionUser({username: "test_user", id: "567", upvoted_ids: []})
DiscussionUtil.setUser(window.user)
@makeModerator = () ->
DiscussionUtil.roleIds["Moderator"].push(parseInt(window.user.id))
@setUnderscoreFixtures = ->
appendSetFixtures("""
<div id="fixture-element"></div>
<!--
NOTE the html markup here comes from rendering lms/templates/discussion/_underscore_templates.html through a
browser and pasting the output. When that file changes, this one should be regenerated alongside it.
-->
<script aria-hidden="true" type="text/template" id="thread-template">
<article class="discussion-article" data-id="<%- id %>">
<div class="thread-wrapper">
<div class="forum-thread-main-wrapper">
<div class="thread-content-wrapper"></div>
<div class="post-extended-content">
<ol class="responses js-marked-answer-list"></ol>
</div>
</div>
<div class="post-extended-content">
<div class="response-count"/>
<div class="add-response">
<button class="button add-response-btn">
<i class="icon icon-reply"></i>
<span class="add-response-btn-text">Add A Response</span>
</button>
</div>
<ol class="responses js-response-list"/>
<div class="response-pagination"/>
<div class="post-status-closed bottom-post-status" style="display: none">
This thread is closed.
</div>
<form class="discussion-reply-new" data-id="<%- id %>">
<h4>Post a response:</h4>
<ul class="discussion-errors"></ul>
<div class="reply-body" data-id="<%- id %>"></div>
<div class="reply-post-control">
<a class="discussion-submit-post control-button" href="#">Submit</a>
</div>
</form>
</div>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="forum-thread-expand"><span class="icon icon-plus"/> Expand discussion</a>
<a href="javascript:void(0)" class="forum-thread-collapse"><span class="icon icon-minus"/> Collapse discussion</a>
</div>
</article>
</script>
<script aria-hidden="true" type="text/template" id="thread-show-template">
<div class="discussion-post">
<header>
<% if (obj.group_id) { %>
<div class="group-visibility-label"><%- obj.group_string%></div>
<% } %>
<div class="post-header-content">
<h1><%- title %></h1>
<p class="posted-details">
<%- thread_type %> posted <span class='timeago' title='<%- created_at %>'><%- created_at %></span> by <%= author_display %>
</p>
<div class="post-labels">
<span class="post-label-pinned"><i class="icon icon-pushpin"></i>Pinned</span>
<span class="post-label-reported"><i class="icon icon-flag"></i>Reported</span>
<span class="post-label-closed"><i class="icon icon-lock"></i>Closed</span>
</div>
</div>
<div class="post-header-actions post-extended-content">
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close']
}
)
%>
</div>
</header>
<div class="post-body"><%- body %></div>
<% if (mode == "tab" && obj.courseware_url) { %>
<div class="post-context"><%
var courseware_link = interpolate('<a href="%s">%s</a>', [courseware_url, _.escape(courseware_title)]);
print(interpolate('(this post is about %(courseware_title_linked)s)', {'courseware_title_linked': courseware_link}, true));
%></div>
<% } %>
</div>
</script>
<script aria-hidden="true" type="text/template" id="thread-edit-template">
<div class="discussion-post edit-post-form">
<h1>Editing post</h1>
<ul class="edit-post-form-errors"></ul>
<div class="form-row">
<label class="sr" for="edit-post-title">Edit post title</label>
<input type="text" id="edit-post-title" class="edit-post-title" name="title" value="<%-title %>" placeholder="Title">
</div>
<div class="form-row">
<div class="edit-post-body" name="body"><%- body %></div>
</div>
<input type="submit" id="edit-post-submit" class="post-update" value="Update post">
<a href="#" class="post-cancel">Cancel</a>
</div>
</script>
<script aria-hidden="true" type="text/template" id="thread-response-template">
<div class="discussion-response"></div>
<a href="#" class="action-show-comments">
<%- interpolate('Show Comments (%(num_comments)s)', {num_comments: comments.length}, true) %>
<i class="icon icon-caret-down"></i>
</a>
<ol class="comments">
<li class="new-comment">
<form class="comment-form" data-id="<%- wmdId %>">
<ul class="discussion-errors"></ul>
<label class="sr" for="add-new-comment">Add a comment</label>
<div class="comment-body" id="add-new-comment" data-id="<%- wmdId %>"
data-placeholder="Add a comment..."></div>
<div class="comment-post-control">
<a class="discussion-submit-comment control-button" href="#">Submit</a>
</div>
</form>
</li>
</ol>
</script>
<script aria-hidden="true" type="text/template" id="thread-response-show-template">
<header>
<div class="response-header-content">
<%= author_display %>
<p class="posted-details">
<span class="timeago" title="<%= created_at %>"><%= created_at %></span>
<% if (obj.endorsement) { %> - <%=
interpolate(
thread.get("thread_type") == "question" ?
(endorsement.username ? "marked as answer %(time_ago)s by %(user)s" : "marked as answer %(time_ago)s") :
(endorsement.username ? "endorsed %(time_ago)s by %(user)s" : "endorsed %(time_ago)s"),
{
'time_ago': '<span class="timeago" title="' + endorsement.time + '">' + endorsement.time + '</span>',
'user': endorser_display
},
true
)%><% } %>
</p>
<div class="post-labels">
<span class="post-label-reported"><i class="icon icon-flag"></i>Reported</span>
</div>
</div>
<div class="response-header-actions">
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'response',
primaryActions: ['vote', thread.get('thread_type') == 'question' ? 'answer' : 'endorse'],
secondaryActions: ['edit', 'delete', 'report']
}
)
%>
</div>
</header>
<div class="response-body"><%- body %></div>
</script>
<script aria-hidden="true" type="text/template" id="thread-response-edit-template">
<div class="edit-post-form">
<h1>Editing response</h1>
<ul class="edit-post-form-errors"></ul>
<div class="form-row">
<div class="edit-post-body" name="body" data-id="<%- id %>"><%- body %></div>
</div>
<input type="submit" id="edit-response-submit"class="post-update" value="Update response">
<a href="#" class="post-cancel">Cancel</a>
</div>
</script>
<script aria-hidden="true" type="text/template" id="response-comment-show-template">
<div id="comment_<%- id %>">
<div class="response-body"><%- body %></div>
<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'comment',
primaryActions: [],
secondaryActions: ['edit', 'delete', 'report']
}
)
%>
<p class="posted-details">
<%=
interpolate(
'posted %(time_ago)s by %(author)s',
{'time_ago': '<span class="timeago" title="' + created_at + '">' + created_at + '</span>', 'author': author_display},
true
)%>
</p>
<div class="post-labels">
<span class="post-label-reported"><i class="icon icon-flag"></i>Reported</span>
</div>
</div>
</script>
<script aria-hidden="true" type="text/template" id="response-comment-edit-template">
<div class="edit-post-form" id="comment_<%- id %>">
<h1>Editing comment</h1>
<ul class="edit-comment-form-errors"></ul>
<div class="form-row">
<div class="edit-comment-body" name="body" data-id="<%- id %>"><%- body %></div>
</div>
<input type="submit" id="edit-comment-submit" class="post-update" value="Update comment">
<a href="#" class="post-cancel">Cancel</a>
</div>
</script>
<script aria-hidden="true" type="text/template" id="thread-list-item-template">
<li data-id="<%- id %>" class="forum-nav-thread<% if (typeof(read) != "undefined" && !read) { %> is-unread<% } %>">
<a href="#" class="forum-nav-thread-link">
<div class="forum-nav-thread-wrapper-0">
<%
var icon_class, sr_text;
if (thread_type == "discussion") {
icon_class = "icon-comments";
sr_text = "discussion";
} else if (endorsed) {
icon_class = "icon-ok";
sr_text = "answered question";
} else {
icon_class = "icon-question";
sr_text = "unanswered question";
}
%>
<span class="sr"><%= sr_text %></span>
<i class="icon <%= icon_class %>"></i>
</div><div class="forum-nav-thread-wrapper-1">
<span class="forum-nav-thread-title"><%- title %></span>
<%
var labels = "";
if (pinned) {
labels += '<li class="post-label-pinned"><i class="icon icon-pushpin"></i>Pinned</li> ';
}
if (typeof(subscribed) != "undefined" && subscribed) {
labels += '<li class="post-label-following"><i class="icon icon-star"></i>Following</li> ';
}
if (staff_authored) {
labels += '<li class="post-label-by-staff"><i class="icon icon-user"></i>By: Staff</li> ';
}
if (community_ta_authored) {
labels += '<li class="post-label-by-community-ta"><i class="icon icon-user"></i>By: Community TA</li> ';
}
if (labels != "") {
print('<ul class="forum-nav-thread-labels">' + labels + '</ul>');
}
%>
</div><div class="forum-nav-thread-wrapper-2">
<span class="forum-nav-thread-votes-count">+<%=
interpolate(
'%(votes_up_count)s%(span_sr_open)s votes %(span_close)s',
{'span_sr_open': '<span class="sr">', 'span_close': '</span>', 'votes_up_count': votes['up_count']},
true
)
%></span>
<span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>">
<%
var fmt;
// Counts in data do not include the post itself, but the UI should
var data = {
'span_sr_open': '<span class="sr">',
'span_close': '</span>',
'unread_comments_count': unread_comments_count + (read ? 0 : 1),
'comments_count': comments_count + 1
};
if (unread_comments_count > 0) {
fmt = '%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s unread comments)%(span_close)s';
} else {
fmt = '%(comments_count)s %(span_sr_open)scomments %(span_close)s';
}
print(interpolate(fmt, data, true));
%>
</span>
</div>
</a>
</li>
</script>
<script aria-hidden="true" type="text/template" id="discussion-home">
<div class="discussion-article blank-slate">
<section class="home-header">
<span class="label">DISCUSSION HOME:</span>
<h1 class="home-title">Cohort Course</h1>
</section>
</div>
</script>
<script aria-hidden="true" type="text/template" id="search-alert-template">
<div class="search-alert" id="search-alert-<%- cid %>">
<div class="search-alert-content">
<p class="message"><%= message %></p>
</div>
<div class="search-alert-controls">
<a href="#" class="dismiss control control-dismiss"><i class="icon icon-remove"></i></a>
</div>
</div>
</script>
<script aria-hidden="true" type="text/template" id="new-post-template">
<form class="forum-new-post-form">
<ul class="post-errors" style="display: none"></ul>
<div class="post-field">
<div class="field-label">
<span class="field-label-text">
Post type:
</span><fieldset class="field-input">
<input type="radio" name="<%= form_id %>-post-type" class="post-type-input" id="<%= form_id %>-post-type-question" value="question" checked>
<label for="<%= form_id %>-post-type-question" class="post-type-label">
<i class="icon icon-question"></i>
Question
</label>
<input type="radio" name="<%= form_id %>-post-type" class="post-type-input" id="<%= form_id %>-post-type-discussion" value="discussion">
<label for="<%= form_id %>-post-type-discussion" class="post-type-label">
<i class="icon icon-comments"></i>
Discussion
</label>
</fieldset>
</div><span class="field-help">
Questions raise issues that need answers. Discussions share ideas and start conversations.
</span>
</div>
<% if (mode=="tab") { %>
<div class="post-field">
<div class="field-label">
<span class="field-label-text">
Topic Area:
</span><div class="field-input post-topic">
<a href="#" class="post-topic-button">
<span class="sr">Discussion topics; current selection is: </span>
<span class="js-selected-topic"></span>
<span class="drop-arrow" aria-hidden="true">▾</span>
</a>
<div class="topic-menu-wrapper">
<label class="topic-filter-label">
<span class="sr">Filter topics</span>
<input type="text" class="topic-filter-input" placeholder="Filter topics">
</label>
<ul class="topic-menu" role="menu"><%= topics_html %></ul>
</div>
</div>
</div><span class="field-help">
Add your post to a relevant topic to help others find it.
</span>
</div>
<% } %>
<% if (cohort_options) { %>
<div class="post-field">
<label class="field-label">
<span class="field-label-text">
Visible To:
</span><select class="field-input js-group-select" name="group_id">
<option value="">All Groups</option>
<% _.each(cohort_options, function(opt) { %>
<option value="<%= opt.value %>" <% if (opt.selected) { %>selected<% } %>><%- opt.text %></option>
<% }); %>
</select>
</label><div class="field-help">
Instructors can set whether a post in a cohorted topic is visible to all cohorts or only to a specific cohort.
</div>
</div>
<% } %>
<div class="post-field">
<label class="field-label">
<span class="sr">Title:</span>
<input type="text" class="field-input js-post-title" name="title" placeholder="Title">
</label><span class="field-help">
Add a clear and descriptive title to encourage participation.
</span>
</div>
<div class="post-field js-post-body editor" name="body" data-placeholder="Enter your question or comment…"></div>
<div class="post-options">
<label class="post-option is-enabled">
<input type="checkbox" name="follow" class="post-option-input js-follow" checked>
<i class="icon icon-star"></i>follow this post
</label>
<% if (allow_anonymous) { %>
<label class="post-option">
<input type="checkbox" name="anonymous" class="post-option-input js-anon">
post anonymously
</label>
<% } %>
<% if (allow_anonymous_to_peers) { %>
<label class="post-option">
<input type="checkbox" name="anonymous_to_peers" class="post-option-input js-anon-peers">
post anonymously to classmates
</label>
<% } %>
</div>
<div>
<input type="submit" class="submit" value="Add Post">
<a href="#" class="cancel">Cancel</a>
</div>
</form>
</script>
<script aria-hidden="true" type="text/template" id="new-post-menu-entry-template">
<li role="menuitem" class="topic-menu-item">
<a href="#" class="topic-title" data-discussion-id="<%- id %>" data-cohorted="<%- is_cohorted %>"><%- text %></a>
</li>
</script>
<script aria-hidden="true" type="text/template" id="new-post-menu-category-template">
<li role="menuitem" class="topic-menu-item">
<span class="topic-title"><%- text %></span>
<ul role="menu" class="topic-submenu"><%= entries %></ul>
</li>
</script>
<script type="text/template" id="forum-action-endorse">
<li class="actions-item">
<a href="javascript:void(0)" class="action-button action-endorse" role="checkbox" aria-checked="false">
<span class="sr">Endorse</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Endorse</span>
<span class="label-checked">Unendorse</span>
</span>
<span class="action-icon"><i class="icon icon-ok"></i></span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-answer">
<li class="actions-item">
<a href="javascript:void(0)" class="action-button action-answer" role="checkbox" aria-checked="false">
<span class="sr">Mark as Answer</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Mark as Answer</span>
<span class="label-checked">Unmark as Answer</span>
</span>
<span class="action-icon"><i class="icon icon-ok"></i></span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-follow">
<li class="actions-item">
<a href="javascript:void(0)" class="action-button action-follow" role="checkbox" aria-checked="false">
<span class="sr">Follow</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Follow</span>
<span class="label-checked">Unfollow</span>
</span>
<span class="action-icon"><i class="icon icon-star"></i></span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-vote">
<li class="actions-item">
<a href="#" class="action-button action-vote" role="checkbox" aria-checked="false">
<span class="sr">Vote</span>
<span class="sr js-sr-vote-count"></span>
<span class="action-label" aria-hidden="true">
<span class="js-visual-vote-count"></span>
</span>
<span class="action-icon" aria-hidden="true">
<i class="icon icon-plus"></i>
</span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-report">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-report" role="checkbox" aria-checked="false">
<span class="sr">Report abuse</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Report</span>
<span class="label-checked">Unreport</span>
</span>
<span class="action-icon">
<i class="icon icon-flag"></i>
</span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-pin">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-pin" role="checkbox" aria-checked="false">
<span class="sr">Pin</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Pin</span>
<span class="label-checked">Unpin</span>
</span>
<span class="action-icon">
<i class="icon icon-pushpin"></i>
</span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-close">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-close" role="checkbox" aria-checked="false">
<span class="sr">Close</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">Close</span>
<span class="label-checked">Open</span>
</span>
<span class="action-icon">
<i class="icon icon-lock"></i>
</span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-edit">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-edit" role="button">
<span class="action-label">Edit</span>
<span class="action-icon"><i class="icon icon-pencil"></i></span>
</a>
</li>
</script>
<script type="text/template" id="forum-action-delete">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-delete" role="button">
<span class="action-label">Delete</span>
<span class="action-icon"><i class="icon icon-remove"></i></span>
</a>
</li>
</script>
<script type="text/template" id="forum-actions">
<ul class="<%= contentType %>-actions-list">
<% _.each(primaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>
<li class="actions-item is-visible">
<div class="more-wrapper">
<a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-<%= contentId %>">
<span class="action-label">More</span>
<span class="action-icon"><i class="icon icon-ellipsis-horizontal"></i></span>
</a>
<div class="actions-dropdown" id="action-menu-<%= contentType %>" aria-expanded="false">
<ul class="actions-dropdown-list">
<% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>
</ul>
</div>
</div>
</li>
</ul>
</script>
<script aria-hidden="true" type="text/template" id="post-user-display-template">
<% if (username) { %>
<a href="<%- user_url %>" class="username"><%- username %></a>
<% if (is_community_ta) { %>
<span class="user-label-community-ta">Community TA</span>
<% } else if (is_staff) { %>
<span class="user-label-staff">Staff</span>
<% } %>
<% } else { %>
anonymous
<% } %>
</script>
""")
describe 'DiscussionUtil', ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
describe "updateWithUndo", ->
it "calls through to safeAjax with correct params, and reverts the model in case of failure", ->
deferred = $.Deferred()
spyOn($, "ajax").andReturn(deferred)
spyOn(DiscussionUtil, "safeAjax").andCallThrough()
model = new Backbone.Model({hello: false, number: 42})
updates = {hello: "world"}
# the ajax request should fire and the model should be updated
res = DiscussionUtil.updateWithUndo(model, updates, {foo: "bar"}, "error message")
expect(DiscussionUtil.safeAjax).toHaveBeenCalled()
expect(model.attributes).toEqual({hello: "world", number: 42})
# the error message callback should be set up correctly
spyOn(DiscussionUtil, "discussionAlert")
DiscussionUtil.safeAjax.mostRecentCall.args[0].error()
expect(DiscussionUtil.discussionAlert).toHaveBeenCalledWith("Sorry", "error message")
# if the ajax call ends in failure, the model state should be reverted
deferred.reject()
expect(model.attributes).toEqual({hello: false, number: 42})
describe "DiscussionContentView", ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures(
"""
<div class="discussion-post">
<header>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class='votes-count-number'>0</span> <span class="sr">votes (click to vote)</span></a>
<h1>Post Title</h1>
<p class="posted-details">
<a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a>
<span title="2013-05-08T17:34:07Z" class="timeago">less than a minute ago</span>
</p>
</header>
<div class="post-body"><p>Post body.</p></div>
<div data-tooltip="Report Misuse" data-role="thread-flag" class="discussion-flag-abuse notflagged">
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
<div data-tooltip="pin this thread" class="admin-pin discussion-pin notpinned">
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
</div>
"""
)
DiscussionSpecHelper.setUnderscoreFixtures()
@threadData = {
id: '01234567',
......@@ -35,7 +16,8 @@ describe "DiscussionContentView", ->
}
@thread = new Thread(@threadData)
@view = new DiscussionContentView({ model: @thread })
@view.setElement($('.discussion-post'))
@view.setElement($('#fixture-element'))
@view.render()
it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist
......@@ -59,15 +41,3 @@ describe "DiscussionContentView", ->
@thread.set("abuse_flaggers",temp_array)
@thread.unflagAbuse()
expect(@thread.get 'abuse_flaggers').toEqual []
it 'renders the vote button properly', ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it 'votes correctly', ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, false)
it 'unvotes correctly', ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, false)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
describe "DiscussionThreadShowView", ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
<div class="admin-pin discussion-pin notpinned" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-pushpin"></i>
<span class="pin-label">Pin Thread</span>
</div>
</div>
"""
)
DiscussionSpecHelper.setUnderscoreFixtures()
@user = DiscussionUtil.getUser()
@threadData = {
id: "dummy",
user_id: user.id,
user_id: @user.id,
username: @user.get('username'),
course_id: $$course_id,
title: "dummy title",
body: "this is a thread",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
votes: {up_count: 42},
thread_type: "discussion",
closed: false,
pinned: false,
type: "thread" # TODO - silly that this needs to be explicitly set
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadShowView({ model: @thread })
@view.setElement($(".discussion-post"))
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
describe "renderPinned", ->
describe "for an unpinned thread", ->
it "renders correctly when pinning is allowed", ->
@thread.updateInfo({ability: {can_openclose: true}})
@view.renderPinned()
pinElem = @view.$(".discussion-pin")
expect(pinElem.length).toEqual(1)
expect(pinElem).not.toHaveClass("pinned")
expect(pinElem).toHaveClass("notpinned")
expect(pinElem.find(".pin-label")).toHaveHtml("Pin Thread")
expect(pinElem).not.toHaveAttr("data-tooltip")
expect(pinElem).toHaveAttr("aria-pressed", "false")
# If pinning is not allowed, the pinning UI is not present, so no
# test is needed
describe "for a pinned thread", ->
beforeEach ->
@thread.set("pinned", true)
it "renders correctly when unpinning is allowed", ->
@thread.updateInfo({ability: {can_openclose: true}})
@view.renderPinned()
pinElem = @view.$(".discussion-pin")
expect(pinElem.length).toEqual(1)
expect(pinElem).toHaveClass("pinned")
expect(pinElem).not.toHaveClass("notpinned")
expect(pinElem.find(".pin-label")).toHaveHtml("Pinned<span class='sr'>, click to unpin</span>")
expect(pinElem).toHaveAttr("data-tooltip", "Click to unpin")
expect(pinElem).toHaveAttr("aria-pressed", "true")
it "renders correctly when unpinning is not allowed", ->
@view.renderPinned()
pinElem = @view.$(".discussion-pin")
expect(pinElem.length).toEqual(1)
expect(pinElem).toHaveClass("pinned")
expect(pinElem).not.toHaveClass("notpinned")
expect(pinElem.find(".pin-label")).toHaveHtml("Pinned")
expect(pinElem).not.toHaveAttr("data-tooltip")
expect(pinElem).not.toHaveAttr("aria-pressed")
it "pinning button activates on appropriate events", ->
DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".admin-pin")
@view.setElement($("#fixture-element"))
@spyOn(@view, "convertMath")
describe "voting", ->
it "renders the vote state correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly via click", ->
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("click"))
it "votes correctly via spacebar", ->
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
it "unvotes correctly via click", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("click"))
it "unvotes correctly via spacebar", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
describe "pinning", ->
expectPinnedRendered = (view, model) ->
pinned = model.get('pinned')
button = view.$el.find(".action-pin")
expect(button.hasClass("is-checked")).toBe(pinned)
expect(button.attr("aria-checked")).toEqual(pinned.toString())
it "renders the pinned state correctly", ->
@view.render()
expectPinnedRendered(@view, @thread)
@thread.set('pinned', false)
@view.render()
expectPinnedRendered(@view, @thread)
@thread.set('pinned', true)
@view.render()
expectPinnedRendered(@view, @thread)
it "exposes the pinning control only to authorized users", ->
@thread.updateInfo({ability: {can_openclose: false}})
@view.render()
expect(@view.$el.find(".action-pin").closest(".is-hidden")).toExist()
@thread.updateInfo({ability: {can_openclose: true}})
@view.render()
expect(@view.$el.find(".action-pin").closest(".is-hidden")).not.toExist()
it "handles events correctly", ->
@view.render()
DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".action-pin")
describe "labels", ->
expectOneElement = (view, selector, visible=true) =>
view.render()
elements = view.$el.find(selector)
expect(elements.length).toEqual(1)
if visible
expect(elements).not.toHaveClass("is-hidden")
else
expect(elements).toHaveClass("is-hidden")
it 'displays the closed label when appropriate', ->
expectOneElement(@view, '.post-label-closed', false)
@thread.set('closed', true)
expectOneElement(@view, '.post-label-closed')
it 'displays the pinned label when appropriate', ->
expectOneElement(@view, '.post-label-pinned', false)
@thread.set('pinned', true)
expectOneElement(@view, '.post-label-pinned')
it 'displays the reported label when appropriate for a non-staff user', ->
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should not be labelled
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported', false)
it 'displays the reported label when appropriate for a flag moderator', ->
DiscussionSpecHelper.makeModerator()
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should still be labelled
@thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported')
describe "author display", ->
beforeEach ->
@thread.set('user_url', 'test_user_url')
checkUserLink = (element, is_ta, is_staff) ->
expect(element.find('a.username').length).toEqual(1)
expect(element.find('a.username').text()).toEqual('test_user')
expect(element.find('a.username').attr('href')).toEqual('test_user_url')
expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0)
expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0)
it "renders correctly for a student-authored thread", ->
$el = $('#fixture-element').html(@view.getAuthorDisplay())
checkUserLink($el, false, false)
it "renders correctly for a community TA-authored thread", ->
@thread.set('community_ta_authored', true)
$el = $('#fixture-element').html(@view.getAuthorDisplay())
checkUserLink($el, true, false)
it "renders correctly for a staff-authored thread", ->
@thread.set('staff_authored', true)
$el = $('#fixture-element').html(@view.getAuthorDisplay())
checkUserLink($el, false, true)
it "renders correctly for an anonymously-authored thread", ->
@thread.set('username', null)
$el = $('#fixture-element').html(@view.getAuthorDisplay())
expect($el.find('a.username').length).toEqual(0)
expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/)
describe "DiscussionThreadView", ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures(
"""
<script type="text/template" id="thread-template">
<article class="discussion-article">
<div class="forum-thread-main-wrapper">
<div class="thread-content-wrapper"></div>
<div class="post-extended-content">
<ol class="responses js-marked-answer-list"></ol>
</div>
</div>
<div class="post-extended-content">
<div class="response-count"></div>
<ol class="responses js-response-list"></ol>
<div class="response-pagination"></div>
</div>
<div class="post-tools">
<a href="javascript:void(0)" class="forum-thread-expand">Expand</a>
<a href="javascript:void(0)" class="forum-thread-collapse">Collapse</a>
</div>
</article>
</script>
<script type="text/template" id="thread-show-template">
<div class="discussion-post">
<div class="post-body"><%- body %></div>
</div>
</script>
<script type="text/template" id="thread-response-template">
<div class="response"></div>
</script>
<div class="thread-fixture"/>
"""
)
DiscussionSpecHelper.setUnderscoreFixtures()
jasmine.Clock.useMock()
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
......@@ -73,7 +42,7 @@ describe "DiscussionThreadView", ->
describe "tab mode", ->
beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $(".thread-fixture"), mode: "tab"})
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "tab"})
describe "response count and pagination", ->
it "correctly render for a thread with no responses", ->
......@@ -114,7 +83,7 @@ describe "DiscussionThreadView", ->
describe "inline mode", ->
beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $(".thread-fixture"), mode: "inline"})
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "inline"})
describe "render", ->
it "shows content that should be visible when collapsed", ->
......@@ -159,7 +128,7 @@ describe "DiscussionThreadView", ->
beforeEach ->
@thread.set("thread_type", "question")
@view = new DiscussionThreadView(
{model: @thread, el: $(".thread-fixture"), mode: "tab"}
{model: @thread, el: $("#fixture-element"), mode: "tab"}
)
renderTestCase = (view, numEndorsed, numNonEndorsed) ->
......@@ -173,8 +142,8 @@ describe "DiscussionThreadView", ->
non_endorsed_resp_total: numNonEndorsed
}
)
expect(view.$(".js-marked-answer-list .response").length).toEqual(numEndorsed)
expect(view.$(".js-response-list .response").length).toEqual(numNonEndorsed)
expect(view.$(".js-marked-answer-list .discussion-response").length).toEqual(numEndorsed)
expect(view.$(".js-response-list .discussion-response").length).toEqual(numNonEndorsed)
assertResponseCountAndPaginationCorrect(
view,
ngettext(
......@@ -209,8 +178,8 @@ describe "DiscussionThreadView", ->
non_endorsed_resp_total: 41
})
@view.$el.find(".load-response-button").click()
expect($(".js-marked-answer-list .response").length).toEqual(3)
expect($(".js-response-list .response").length).toEqual(6)
expect($(".js-marked-answer-list .discussion-response").length).toEqual(3)
expect($(".js-response-list .discussion-response").length).toEqual(6)
assertResponseCountAndPaginationCorrect(
@view,
"41 other responses",
......
......@@ -10,107 +10,54 @@ class @DiscussionViewSpecHelper
unread_comments_count: 0,
comments_count: 0,
abuse_flaggers: [],
body: ""
body: "",
title: "dummy title",
created_at: "2014-08-18T01:02:03Z"
}
$.extend(thread, props)
@expectVoteRendered = (view, voted) ->
button = view.$el.find(".vote-btn")
if voted
expect(button.hasClass("is-cast")).toBe(true)
expect(button.attr("aria-pressed")).toEqual("true")
expect(button.attr("data-tooltip")).toEqual("remove vote")
expect(button.text()).toEqual("43 votes (click to remove your vote)")
else
expect(button.hasClass("is-cast")).toBe(false)
expect(button.attr("aria-pressed")).toEqual("false")
expect(button.attr("data-tooltip")).toEqual("vote")
expect(button.text()).toEqual("42 votes (click to vote)")
@expectVoteRendered = (view, model, user) ->
button = view.$el.find(".action-vote")
expect(button.hasClass("is-checked")).toBe(user.voted(model))
expect(button.attr("aria-checked")).toEqual(user.voted(model).toString())
expect(button.find(".js-visual-vote-count").text()).toMatch("^#{model.get('votes').up_count} Votes?$")
expect(button.find(".sr.js-sr-vote-count").text()).toMatch("^currently #{model.get('votes').up_count} votes?$")
@checkRenderVote = (view, model) ->
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
view.render()
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
window.user.vote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, true)
view.render()
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
window.user.unvote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
@checkVote = (view, model, modelData, checkRendering) ->
view.renderVote()
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "43"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
)
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
@checkUnvote = (view, model, modelData, checkRendering) ->
window.user.vote(model)
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
view.render()
DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user)
triggerVoteEvent = (view, event, expectedUrl) ->
deferred = $.Deferred()
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "42"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
expect(params.url.toString()).toEqual(expectedUrl)
return deferred
)
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
view.render()
view.$el.find(".action-vote").trigger(event)
expect($.ajax).toHaveBeenCalled()
@checkToggleVote = (view, model) ->
event = {preventDefault: ->}
spyOn(event, "preventDefault")
spyOn(view, "vote").andCallFake(() -> window.user.vote(model))
spyOn(view, "unvote").andCallFake(() -> window.user.unvote(model))
expect(window.user.voted(model)).toBe(false)
view.toggleVote(event)
expect(view.vote).toHaveBeenCalled()
expect(view.unvote).not.toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(1)
view.vote.reset()
view.unvote.reset()
expect(window.user.voted(model)).toBe(true)
view.toggleVote(event)
expect(view.vote).not.toHaveBeenCalled()
expect(view.unvote).toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(2)
deferred.resolve()
@checkUpvote = (view, model, user, event) ->
expect(model.id in user.get('upvoted_ids')).toBe(false)
initialVoteCount = model.get('votes').up_count
triggerVoteEvent(view, event, DiscussionUtil.urlFor("upvote_#{model.get('type')}", model.id) + "?ajax=1")
expect(model.id in user.get('upvoted_ids')).toBe(true)
expect(model.get('votes').up_count).toEqual(initialVoteCount + 1)
@checkUnvote = (view, model, user, event) ->
user.vote(model)
expect(model.id in user.get('upvoted_ids')).toBe(true)
initialVoteCount = model.get('votes').up_count
triggerVoteEvent(view, event, DiscussionUtil.urlFor("undo_vote_for_#{model.get('type')}", model.id) + "?ajax=1")
expect(user.get('upvoted_ids')).toEqual([])
expect(model.get('votes').up_count).toEqual(initialVoteCount - 1)
@checkButtonEvents = (view, viewFunc, buttonSelector) ->
spy = spyOn(view, viewFunc)
......@@ -126,7 +73,7 @@ class @DiscussionViewSpecHelper
expect(spy).toHaveBeenCalled()
@checkVoteButtonEvents = (view) ->
@checkButtonEvents(view, "toggleVote", ".vote-btn")
@checkButtonEvents(view, "toggleVote", ".action-vote")
@setNextResponseContent = (content) ->
$.ajax.andCallFake(
......
......@@ -2,25 +2,7 @@ describe 'ResponseCommentShowView', ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
# set up the container for the response to go in
setFixtures """
<ol class="responses"></ol>
<script id="response-comment-show-template" type="text/template">
<div id="comment_<%- id %>">
<div class="response-body"><%- body %></div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
<i class="icon"></i><span class="flag-label"></span></div>
<div style="display:none" class="discussion-delete-comment action-delete" data-role="comment-delete" data-tooltip="Delete Comment" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-remove"></i><span class="sr delete-label">Delete Comment</span></div>
<div style="display:none" class="discussion-edit-comment action-edit" data-tooltip="Edit Comment" role="button" tabindex="0">
<i class="icon icon-pencil"></i><span class="sr">Edit Comment</span></div>
<p class="posted-details">&ndash;posted <span class="timeago" title="<%- created_at %>"><%- created_at %></span> by
<% if (obj.username) { %>
<a href="<%- user_url %>" class="profile-link"><%- username %></a>
<% } else {print('anonymous');} %>
</p>
</div>
</script>
"""
DiscussionSpecHelper.setUnderscoreFixtures()
# set up a model for a new Comment
@comment = new Comment {
......@@ -47,11 +29,6 @@ describe 'ResponseCommentShowView', ->
beforeEach ->
spyOn(@view, 'renderAttrs')
spyOn(@view, 'markAsStaff')
it 'produces the correct HTML', ->
@view.render()
expect(@view.el.innerHTML).toContain('"discussion-flag-abuse notflagged"')
it 'can be flagged for abuse', ->
@comment.flagAbuse()
......@@ -91,3 +68,35 @@ describe 'ResponseCommentShowView', ->
@view.bind "comment:edit", triggerTarget
@view.edit()
expect(triggerTarget).toHaveBeenCalled()
describe "labels", ->
expectOneElement = (view, selector, visible=true) =>
view.render()
elements = view.$el.find(selector)
expect(elements.length).toEqual(1)
if visible
expect(elements).not.toHaveClass("is-hidden")
else
expect(elements).toHaveClass("is-hidden")
it 'displays the reported label when appropriate for a non-staff user', ->
@comment.set('abuse_flaggers', [])
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should not be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported', false)
it 'displays the reported label when appropriate for a flag moderator', ->
DiscussionSpecHelper.makeModerator()
@comment.set('abuse_flaggers', [])
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should still be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported')
......@@ -10,19 +10,9 @@ describe 'ResponseCommentView', ->
abuse_flaggers: ['123']
roles: ['Student']
}
setFixtures """
<script id="response-comment-show-template" type="text/template">
<div id="response-comment-show-div"/>
</script>
<script id="response-comment-edit-template" type="text/template">
<div id="response-comment-edit-div">
<div class="edit-comment-body"><textarea/></div>
<ul class="edit-comment-form-errors"/>
</div>
</script>
<div id="response-comment-fixture"/>
"""
@view = new ResponseCommentView({ model: @comment, el: $("#response-comment-fixture") })
DiscussionSpecHelper.setUnderscoreFixtures()
@view = new ResponseCommentView({ model: @comment, el: $("#fixture-element") })
spyOn(ResponseCommentShowView.prototype, "convertMath")
spyOn(DiscussionUtil, "makeWmdEditor")
@view.render()
......@@ -95,8 +85,7 @@ describe 'ResponseCommentView', ->
expect(@view._delete).toHaveBeenCalled()
@view.showView.trigger "comment:edit", makeEventSpy()
expect(@view.edit).toHaveBeenCalled()
expect(@view.$("#response-comment-show-div").length).toEqual(1)
expect(@view.$("#response-comment-edit-div").length).toEqual(0)
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form")
describe 'renderEditView', ->
it 'renders the edit view, removes the show view, and registers event handlers', ->
......@@ -107,8 +96,7 @@ describe 'ResponseCommentView', ->
expect(@view.update).toHaveBeenCalled()
@view.editView.trigger "comment:cancel_edit", makeEventSpy()
expect(@view.cancelEdit).toHaveBeenCalled()
expect(@view.$("#response-comment-show-div").length).toEqual(0)
expect(@view.$("#response-comment-edit-div").length).toEqual(1)
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form")
describe 'edit', ->
it 'triggers the appropriate event and switches to the edit view', ->
......@@ -135,6 +123,8 @@ describe 'ResponseCommentView', ->
describe 'update', ->
beforeEach ->
@updatedBody = "updated body"
# Markdown code creates the editor, so we simulate that here
@view.$el.find(".edit-comment-body").html($("<textarea></textarea>"))
@view.$el.find(".edit-comment-body textarea").val(@updatedBody)
spyOn(@view, 'cancelEdit')
spyOn($, "ajax").andCallFake(
......
describe "ThreadResponseShowView", ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures(
"""
<script type="text/template" id="thread-response-show-template">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"></a>
<a
href="javascript:void(0)"
class="endorse-btn action-endorse <%= thread.get('thread_type') == 'question' ? 'mark-answer' : '' %>"
style="cursor: default; display: none;"
data-tooltip="<%= thread.get('thread_type') == 'question' ? 'mark as answer' : 'endorse' %>"
>
<span class="check-icon" style="pointer-events: none; "></span>
</a>
<p class="posted-details">
<span class="timeago" title="<%= created_at %>"><%= created_at %></span>
<% if (thread.get('thread_type') == 'question' && obj.endorsement) { %> -
<%=
interpolate(
endorsement.username ? "marked as answer %(time_ago)s by %(user)s" : "marked as answer %(time_ago)s",
{
'time_ago': '<span class="timeago" title="' + endorsement.time + '">' + endorsement.time + '</span>',
'user': endorsement.username
},
true
)
%>
<% } %>
</p>
</script>
<div class="discussion-post"></div>
"""
)
DiscussionSpecHelper.setUnderscoreFixtures()
@user = DiscussionUtil.getUser()
@thread = new Thread({"thread_type": "discussion"})
@commentData = {
id: "dummy",
......@@ -43,32 +13,34 @@ describe "ThreadResponseShowView", ->
created_at: "2013-04-03T20:08:39Z",
endorsed: false,
abuse_flaggers: [],
votes: {up_count: "42"}
votes: {up_count: 42},
type: "comment"
}
@comment = new Comment(@commentData)
@comment.set("thread", @thread)
@view = new ThreadResponseShowView({ model: @comment })
@view.setElement($(".discussion-post"))
@view = new ThreadResponseShowView({ model: @comment, $el: $("#fixture-element") })
# Avoid unnecessary boilerplate
spyOn(ThreadResponseShowView.prototype, "convertMath")
@view.render()
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
describe "voting", ->
it "renders the vote state correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @comment, @commentData, true)
it "votes correctly via click", ->
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("click"))
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @commentData, true)
it "votes correctly via spacebar", ->
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("keydown", {which: 32}))
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @comment)
it "unvotes correctly via click", ->
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @user, $.Event("click"))
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
it "unvotes correctly via spacebar", ->
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @user, $.Event("keydown", {which: 32}))
it "renders endorsement correctly for a marked answer in a question thread", ->
endorsement = {
......@@ -81,7 +53,7 @@ describe "ThreadResponseShowView", ->
"endorsement": endorsement
})
@view.render()
expect(@view.$(".posted-details").text()).toMatch(
expect(@view.$(".posted-details").text().replace(/\s+/g, " ")).toMatch(
"marked as answer less than a minute ago by " + endorsement.username
)
......@@ -97,17 +69,45 @@ describe "ThreadResponseShowView", ->
})
@view.render()
expect(@view.$(".posted-details").text()).toMatch("marked as answer less than a minute ago")
expect(@view.$(".posted-details").text()).not.toMatch(" by ")
expect(@view.$(".posted-details").text()).not.toMatch("\sby\s")
it "renders endorsement correctly for an endorsed response in a discussion thread", ->
endorsement = {
"username": "test_endorser",
"time": new Date().toISOString()
}
@thread.set("thread_type", "discussion")
@comment.set({
"endorsed": true,
"endorsement": endorsement
})
@view.render()
expect(@view.$(".posted-details").text().replace(/\s+/g, " ")).toMatch(
"endorsed less than a minute ago by " + endorsement.username
)
it "renders anonymous endorsement correctly for an endorsed response in a discussion thread", ->
endorsement = {
"username": null,
"time": new Date().toISOString()
}
@thread.set("thread_type", "discussion")
@comment.set({
"endorsed": true,
"endorsement": endorsement
})
@view.render()
expect(@view.$(".posted-details").text()).toMatch("endorsed less than a minute ago")
expect(@view.$(".posted-details").text()).not.toMatch("\sby\s")
it "re-renders correctly when endorsement changes", ->
DiscussionUtil.loadRoles({"Moderator": [parseInt(window.user.id)]})
@thread.set("thread_type", "question")
@view.render()
expect(@view.$(".posted-details").text()).not.toMatch("marked as answer")
@view.$(".action-endorse").click()
expect(@view.$(".posted-details").text()).toMatch(
"marked as answer less than a minute ago by " + user.get("username")
)
@view.$(".action-endorse").click()
@view.$(".action-answer").click()
expect(@view.$(".posted-details").text()).toMatch("marked as answer")
@view.$(".action-answer").click()
expect(@view.$(".posted-details").text()).not.toMatch("marked as answer")
it "allows a moderator to mark an answer in a question thread", ->
......@@ -117,12 +117,11 @@ describe "ThreadResponseShowView", ->
"user_id": (parseInt(window.user.id) + 1).toString()
})
@view.render()
endorseButton = @view.$(".action-endorse")
endorseButton = @view.$(".action-answer")
expect(endorseButton.length).toEqual(1)
expect(endorseButton).not.toHaveCss({"display": "none"})
expect(endorseButton).toHaveClass("is-clickable")
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
endorseButton.click()
expect(endorseButton).toHaveClass("is-endorsed")
expect(endorseButton).toHaveClass("is-checked")
it "allows the author of a question thread to mark an answer", ->
@thread.set({
......@@ -130,12 +129,11 @@ describe "ThreadResponseShowView", ->
"user_id": window.user.id
})
@view.render()
endorseButton = @view.$(".action-endorse")
endorseButton = @view.$(".action-answer")
expect(endorseButton.length).toEqual(1)
expect(endorseButton).not.toHaveCss({"display": "none"})
expect(endorseButton).toHaveClass("is-clickable")
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
endorseButton.click()
expect(endorseButton).toHaveClass("is-endorsed")
expect(endorseButton).toHaveClass("is-checked")
it "does not allow the author of a discussion thread to endorse", ->
@thread.set({
......@@ -145,10 +143,7 @@ describe "ThreadResponseShowView", ->
@view.render()
endorseButton = @view.$(".action-endorse")
expect(endorseButton.length).toEqual(1)
expect(endorseButton).toHaveCss({"display": "none"})
expect(endorseButton).not.toHaveClass("is-clickable")
endorseButton.click()
expect(endorseButton).not.toHaveClass("is-endorsed")
expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden")
it "does not allow a student who is not the author of a question thread to mark an answer", ->
@thread.set({
......@@ -156,9 +151,70 @@ describe "ThreadResponseShowView", ->
"user_id": (parseInt(window.user.id) + 1).toString()
})
@view.render()
endorseButton = @view.$(".action-endorse")
endorseButton = @view.$(".action-answer")
expect(endorseButton.length).toEqual(1)
expect(endorseButton).toHaveCss({"display": "none"})
expect(endorseButton).not.toHaveClass("is-clickable")
endorseButton.click()
expect(endorseButton).not.toHaveClass("is-endorsed")
expect(endorseButton.closest(".actions-item")).toHaveClass("is-hidden")
describe "labels", ->
expectOneElement = (view, selector, visible=true) =>
view.render()
elements = view.$el.find(selector)
expect(elements.length).toEqual(1)
if visible
expect(elements).not.toHaveClass("is-hidden")
else
expect(elements).toHaveClass("is-hidden")
it 'displays the reported label when appropriate for a non-staff user', ->
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should not be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported', false)
it 'displays the reported label when appropriate for a flag moderator', ->
DiscussionSpecHelper.makeModerator()
expectOneElement(@view, '.post-label-reported', false)
# flagged by current user - should be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id])
expectOneElement(@view, '.post-label-reported')
# flagged by some other user but not the current one - should still be labelled
@comment.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1])
expectOneElement(@view, '.post-label-reported')
describe "endorser display", ->
beforeEach ->
@comment.set('endorsement', {
"username": "test_endorser",
"time": new Date().toISOString()
})
spyOn(DiscussionUtil, 'urlFor').andReturn('test_endorser_url')
checkUserLink = (element, is_ta, is_staff) ->
expect(element.find('a.username').length).toEqual(1)
expect(element.find('a.username').text()).toEqual('test_endorser')
expect(element.find('a.username').attr('href')).toEqual('test_endorser_url')
expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0)
expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0)
it "renders nothing when the response has not been endorsed", ->
@comment.set('endorsement', null)
expect(@view.getEndorserDisplay()).toBeNull()
it "renders correctly for a student-endorsed response", ->
$el = $('#fixture-element').html(@view.getEndorserDisplay())
checkUserLink($el, false, false)
it "renders correctly for a community TA-endorsed response", ->
spyOn(DiscussionUtil, 'isTA').andReturn(true)
$el = $('#fixture-element').html(@view.getEndorserDisplay())
checkUserLink($el, true, false)
it "renders correctly for a staff-endorsed response", ->
spyOn(DiscussionUtil, 'isStaff').andReturn(true)
$el = $('#fixture-element').html(@view.getEndorserDisplay())
checkUserLink($el, false, true)
describe 'ThreadResponseView', ->
beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures """
<script id="thread-response-template" type="text/template">
<a href="#" class="action-show-comments">Show comments</a>
<ol class="comments"></ol>
</script>
<div id="thread-response-fixture"/>
"""
DiscussionSpecHelper.setUnderscoreFixtures()
@response = new Comment {
children: [{}, {}]
}
@view = new ThreadResponseView({model: @response, el: $("#thread-response-fixture")})
@view = new ThreadResponseView({model: @response, el: $("#fixture-element")})
spyOn(ThreadResponseShowView.prototype, "render")
spyOn(ResponseCommentView.prototype, "render")
......@@ -24,7 +19,7 @@ describe 'ThreadResponseView', ->
it 'hides "show comments" link if collapseComments is set but response has no comments', ->
@response = new Comment { children: [] }
@view = new ThreadResponseView({
model: @response, el: $("#thread-response-fixture"),
model: @response, el: $("#fixture-element"),
collapseComments: true
})
@view.render()
......@@ -33,7 +28,7 @@ describe 'ThreadResponseView', ->
it 'hides comments if collapseComments is set and shows them when "show comments" link is clicked', ->
@view = new ThreadResponseView({
model: @response, el: $("#thread-response-fixture"),
model: @response, el: $("#fixture-element"),
collapseComments: true
})
@view.render()
......
......@@ -108,13 +108,21 @@ if Backbone?
@get("abuse_flaggers").pop(window.user.get('id'))
@trigger "change", @
isFlagged: ->
user = DiscussionUtil.getUser()
flaggers = @get("abuse_flaggers")
user and (user.id in flaggers or (DiscussionUtil.isPrivilegedUser(user.id) and flaggers.length > 0))
incrementVote: (increment) ->
newVotes = _.clone(@get("votes"))
newVotes.up_count = newVotes.up_count + increment
@set("votes", newVotes)
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
@incrementVote(1)
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
@incrementVote(-1)
class @Thread extends @Content
urlMappers:
......
......@@ -21,15 +21,14 @@ class @DiscussionUtil
@setUser: (user) ->
@user = user
@getUser: () ->
@user
@loadRoles: (roles)->
@roleIds = roles
@loadFlagModerator: (what)->
@isFlagModerator = ((what=="True") or (what == 1))
@loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@loadFlagModerator($("#discussion-container").data("flag-moderator"))
@isStaff: (user_id) ->
user_id ?= @user?.id
......@@ -162,6 +161,13 @@ class @DiscussionUtil
params["$loading"].loaded()
return request
@updateWithUndo: (model, updates, safeAjaxParams, errorMsg) ->
if errorMsg
safeAjaxParams.error = => @discussionAlert(gettext("Sorry"), errorMsg)
undo = _.pick(model.attributes, _.keys(updates))
model.set(updates)
@safeAjax(safeAjaxParams).fail(() -> model.set(undo))
@bindLocalEvents: ($local, eventsHandler) ->
for eventSelector, handler of eventsHandler
[event, selector] = eventSelector.split(' ')
......
......@@ -8,30 +8,6 @@ if Backbone?
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
attrRenderer:
closed: (closed) ->
return if not @$(".action-openclose").length
return if not @$(".post-status-closed").length
if closed
@$(".post-status-closed").show()
@$(".action-openclose").html(@$(".action-openclose").html().replace(gettext("Close"), gettext("Open")))
@$(".discussion-reply-new").hide()
else
@$(".post-status-closed").hide()
@$(".action-openclose").html(@$(".action-openclose").html().replace(gettext("Open"), gettext("Close")))
@$(".discussion-reply-new").show()
voted: (voted) ->
votes_point: (votes_point) ->
comments_count: (comments_count) ->
subscribed: (subscribed) ->
if subscribed
@$(".dogear").addClass("is-followed").attr("aria-checked", "true")
else
@$(".dogear").removeClass("is-followed").attr("aria-checked", "false")
ability: (ability) ->
for action, selector of @abilityRenderer
if not ability[action]
......@@ -41,14 +17,22 @@ if Backbone?
abilityRenderer:
editable:
enable: -> @$(".action-edit").closest("li").show()
disable: -> @$(".action-edit").closest("li").hide()
enable: -> @$(".action-edit").closest(".actions-item").removeClass("is-hidden")
disable: -> @$(".action-edit").closest(".actions-item").addClass("is-hidden")
can_delete:
enable: -> @$(".action-delete").closest("li").show()
disable: -> @$(".action-delete").closest("li").hide()
enable: -> @$(".action-delete").closest(".actions-item").removeClass("is-hidden")
disable: -> @$(".action-delete").closest(".actions-item").addClass("is-hidden")
can_openclose:
enable: -> @$(".action-openclose").closest("li").show()
disable: -> @$(".action-openclose").closest("li").hide()
enable: ->
_.each(
[".action-close", ".action-pin"],
(selector) => @$(selector).closest(".actions-item").removeClass("is-hidden")
)
disable: ->
_.each(
[".action-close", ".action-pin"],
(selector) => @$(selector).closest(".actions-item").addClass("is-hidden")
)
renderPartialAttrs: ->
for attr, value of @model.changedAttributes()
......@@ -76,114 +60,237 @@ if Backbone?
initialize: ->
@model.bind('change', @renderPartialAttrs, @)
toggleFollowing: (event) =>
@listenTo(@model, "change:endorsed", =>
if @model instanceof Comment
@trigger("comment:endorse")
)
class @DiscussionContentShowView extends DiscussionContentView
events:
_.reduce(
[
[".action-follow", "toggleFollow"],
[".action-answer", "toggleEndorse"],
[".action-endorse", "toggleEndorse"],
[".action-vote", "toggleVote"],
[".action-more", "toggleSecondaryActions"],
[".action-pin", "togglePin"],
[".action-edit", "edit"],
[".action-delete", "_delete"],
[".action-report", "toggleReport"],
[".action-close", "toggleClose"],
],
(obj, event) =>
selector = event[0]
funcName = event[1]
obj["click #{selector}"] = (event) -> @[funcName](event)
obj["keydown #{selector}"] = (event) -> DiscussionUtil.activateOnSpace(event, @[funcName])
obj
,
{}
)
updateButtonState: (selector, checked) =>
$button = @$(selector)
$button.toggleClass("is-checked", checked)
$button.attr("aria-checked", checked)
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
subscribed: (subscribed) ->
@updateButtonState(".action-follow", subscribed)
endorsed: (endorsed) ->
selector = if @model.get("thread").get("thread_type") == "question" then ".action-answer" else ".action-endorse"
@updateButtonState(selector, endorsed)
$button = @$(selector)
$button.closest(".actions-item").toggleClass("is-hidden", not @model.canBeEndorsed())
$button.toggleClass("is-checked", endorsed)
votes: (votes) ->
selector = ".action-vote"
@updateButtonState(selector, window.user.voted(@model))
button = @$el.find(selector)
numVotes = votes.up_count
button.find(".js-sr-vote-count").html(
interpolate(
ngettext("currently %(numVotes)s vote", "currently %(numVotes)s votes", numVotes),
{numVotes: numVotes},
true
)
)
button.find(".js-visual-vote-count").html(
interpolate(
ngettext("%(numVotes)s Vote", "%(numVotes)s Votes", numVotes),
{numVotes: numVotes},
true
)
)
pinned: (pinned) ->
@updateButtonState(".action-pin", pinned)
@$(".post-label-pinned").toggleClass("is-hidden", not pinned)
abuse_flaggers: (abuse_flaggers) ->
flagged = @model.isFlagged()
@updateButtonState(".action-report", flagged)
@$(".post-label-reported").toggleClass("is-hidden", not flagged)
closed: (closed) ->
@updateButtonState(".action-close", closed)
@$(".post-label-closed").toggleClass("is-hidden", not closed)
})
toggleSecondaryActions: (event) =>
event.preventDefault()
$elem = $(event.target)
url = null
if not @model.get('subscribed')
@model.follow()
url = @model.urlFor("follow")
event.stopPropagation()
@secondaryActionsExpanded = !@secondaryActionsExpanded
@$(".action-more").toggleClass("is-expanded", @secondaryActionsExpanded)
@$(".actions-dropdown").
toggleClass("is-expanded", @secondaryActionsExpanded).
attr("aria-expanded", @secondaryActionsExpanded)
if @secondaryActionsExpanded
if event.type == "keydown"
@$(".action-list-item:first").focus()
$("body").on("click", @toggleSecondaryActions)
$("body").on("keydown", @handleSecondaryActionEscape)
@$(".action-list-item").on("blur", @handleSecondaryActionBlur)
else
@model.unfollow()
url = @model.urlFor("unfollow")
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
toggleFlagAbuse: (event) =>
$("body").off("click", @toggleSecondaryActions)
$("body").off("keydown", @handleSecondaryActionEscape)
@$(".action-list-item").off("blur", @handleSecondaryActionBlur)
handleSecondaryActionEscape: (event) =>
if event.keyCode == 27 # Esc
@toggleSecondaryActions(event)
@$(".action-more").focus()
handleSecondaryActionBlur: (event) =>
setTimeout(
=>
if @secondaryActionsExpanded && @$(".actions-dropdown :focus").length == 0
@toggleSecondaryActions(event)
,
10
)
toggleFollow: (event) =>
event.preventDefault()
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@unFlagAbuse()
is_subscribing = not @model.get("subscribed")
url = @model.urlFor(if is_subscribing then "follow" else "unfollow")
if is_subscribing
msg = gettext("We had some trouble subscribing you to this thread. Please try again.")
else
@flagAbuse()
flagAbuse: =>
url = @model.urlFor("flagAbuse")
DiscussionUtil.safeAjax
$elem: @$(".discussion-flag-abuse")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
###
note, we have to clone the array in order to trigger a change event
###
temp_array = _.clone(@model.get('abuse_flaggers'));
temp_array.push(window.user.id)
@model.set('abuse_flaggers', temp_array)
unFlagAbuse: =>
url = @model.urlFor("unFlagAbuse")
DiscussionUtil.safeAjax
$elem: @$(".discussion-flag-abuse")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
temp_array = _.clone(@model.get('abuse_flaggers'));
temp_array.pop(window.user.id)
# if you're an admin, clear this
if DiscussionUtil.isFlagModerator
temp_array = []
@model.set('abuse_flaggers', temp_array)
renderVote: =>
button = @$el.find(".vote-btn")
voted = window.user.voted(@model)
voteNum = @model.get("votes")["up_count"]
button.toggleClass("is-cast", voted)
button.attr("aria-pressed", voted)
button.attr("data-tooltip", if voted then gettext("remove vote") else gettext("vote"))
buttonTextFmt =
if voted
ngettext(
"vote (click to remove your vote)",
"votes (click to remove your vote)",
voteNum
)
else
ngettext(
"vote (click to vote)",
"votes (click to vote)",
voteNum
)
buttonTextFmt = "%(voteNum)s%(startSrSpan)s " + buttonTextFmt + "%(endSrSpan)s"
buttonText = interpolate(
buttonTextFmt,
{voteNum: voteNum, startSrSpan: "<span class='sr'>", endSrSpan: "</span>"},
true
msg = gettext("We had some trouble unsubscribing you from this thread. Please try again.")
DiscussionUtil.updateWithUndo(
@model,
{"subscribed": is_subscribing},
{url: url, type: "POST", $elem: $(event.currentTarget)},
msg
)
button.html("<span class='plus-icon'/>" + buttonText)
toggleEndorse: (event) =>
event.preventDefault()
is_endorsing = not @model.get("endorsed")
url = @model.urlFor("endorse")
updates =
endorsed: is_endorsing
endorsement: if is_endorsing then {username: DiscussionUtil.getUser().get("username"), time: new Date().toISOString()} else null
if @model.get('thread').get('thread_type') == 'question'
if is_endorsing
msg = gettext("We had some trouble marking this response as an answer. Please try again.")
else
msg = gettext("We had some trouble removing this response as an answer. Please try again.")
else
if is_endorsing
msg = gettext("We had some trouble marking this response endorsed. Please try again.")
else
msg = gettext("We had some trouble removing this endorsement. Please try again.")
beforeFunc = () => @trigger("comment:endorse")
DiscussionUtil.updateWithUndo(
@model,
updates,
{url: url, type: "POST", data: {endorsed: is_endorsing}, beforeSend: beforeFunc, $elem: $(event.currentTarget)},
msg
).always(@trigger("comment:endorse")) # ensures UI components get updated to the correct state when ajax completes
toggleVote: (event) =>
event.preventDefault()
if window.user.voted(@model)
@unvote()
user = DiscussionUtil.getUser()
is_voting = not user.voted(@model)
url = @model.urlFor(if is_voting then "upvote" else "unvote")
updates =
upvoted_ids: (if is_voting then _.union else _.difference)(user.get('upvoted_ids'), [@model.id])
DiscussionUtil.updateWithUndo(
user,
updates,
{url: url, type: "POST", $elem: $(event.currentTarget)},
gettext("We had some trouble saving your vote. Please try again.")
).done(() => if is_voting then @model.vote() else @model.unvote())
togglePin: (event) =>
event.preventDefault()
is_pinning = not @model.get("pinned")
url = @model.urlFor(if is_pinning then "pinThread" else "unPinThread")
if is_pinning
msg = gettext("We had some trouble pinning this thread. Please try again.")
else
msg = gettext("We had some trouble unpinning this thread. Please try again.")
DiscussionUtil.updateWithUndo(
@model,
{pinned: is_pinning},
{url: url, type: "POST", $elem: $(event.currentTarget)},
msg
)
toggleReport: (event) =>
event.preventDefault()
if @model.isFlagged()
is_flagging = false
msg = gettext("We had some trouble removing your flag on this post. Please try again.")
else
is_flagging = true
msg = gettext("We had some trouble reporting this post. Please try again.")
url = @model.urlFor(if is_flagging then "flagAbuse" else "unFlagAbuse")
updates =
abuse_flaggers: (if is_flagging then _.union else _.difference)(@model.get("abuse_flaggers"), [DiscussionUtil.getUser().id])
DiscussionUtil.updateWithUndo(
@model,
updates,
{url: url, type: "POST", $elem: $(event.currentTarget)},
msg
)
toggleClose: (event) =>
event.preventDefault()
is_closing = not @model.get('closed')
if is_closing
msg = gettext("We had some trouble closing this thread. Please try again.")
else
msg = gettext("We had some trouble reopening this thread. Please try again.")
updates = {closed: is_closing}
DiscussionUtil.updateWithUndo(
@model,
updates,
{url: @model.urlFor("close"), type: "POST", data: updates, $elem: $(event.currentTarget)},
msg
)
getAuthorDisplay: ->
_.template($("#post-user-display-template").html())(
username: @model.get('username') || null
user_url: @model.get('user_url')
is_community_ta: @model.get('community_ta_authored')
is_staff: @model.get('staff_authored')
)
getEndorserDisplay: ->
endorsement = @model.get('endorsement')
if endorsement and endorsement.username
_.template($("#post-user-display-template").html())(
username: endorsement.username
user_url: DiscussionUtil.urlFor('user_profile', endorsement.user_id)
is_community_ta: DiscussionUtil.isTA(endorsement.user_id)
is_staff: DiscussionUtil.isStaff(endorsement.user_id)
)
else
@vote()
vote: =>
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: =>
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
null
if Backbone?
class @DiscussionThreadShowView extends DiscussionContentView
events:
"click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnSpace(event, @toggleVote)
"click .discussion-flag-abuse": "toggleFlagAbuse"
"keydown .discussion-flag-abuse":
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
"click .admin-pin":
(event) -> @togglePin(event)
"keydown .admin-pin":
(event) -> DiscussionUtil.activateOnSpace(event, @togglePin)
"click .action-follow": "toggleFollowing"
"keydown .action-follow":
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFollowing)
"click .action-edit": "edit"
"click .action-delete": "_delete"
"click .action-openclose": "toggleClosed"
$: (selector) ->
@$el.find(selector)
class @DiscussionThreadShowView extends DiscussionContentShowView
initialize: (options) ->
super()
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode)
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-show-template").html())
context = @model.toJSON()
context.mode = @mode
context = $.extend(
{
mode: @mode,
flagged: @model.isFlagged(),
author_display: @getAuthorDisplay(),
cid: @model.cid
},
@model.attributes,
)
@template(context)
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderVote()
@renderFlagged()
@renderPinned()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
......@@ -49,60 +29,6 @@ if Backbone?
@highlight @$("h1,h3")
@
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Click to remove report"))
###
Translators: The text between start_sr_span and end_span is not shown
in most browsers but will be read by screen readers.
###
@$(".discussion-flag-abuse .flag-label").html(interpolate(gettext("Misuse Reported%(start_sr_span)s, click to remove report%(end_span)s"), {"start_sr_span": "<span class='sr'>", "end_span": "</span>"}, true))
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
renderPinned: =>
pinElem = @$(".discussion-pin")
pinLabelElem = pinElem.find(".pin-label")
if @model.get("pinned")
pinElem.addClass("pinned")
pinElem.removeClass("notpinned")
if @model.can("can_openclose")
###
Translators: The text between start_sr_span and end_span is not shown
in most browsers but will be read by screen readers.
###
pinLabelElem.html(
interpolate(
gettext("Pinned%(start_sr_span)s, click to unpin%(end_span)s"),
{"start_sr_span": "<span class='sr'>", "end_span": "</span>"},
true
)
)
pinElem.attr("data-tooltip", gettext("Click to unpin"))
pinElem.attr("aria-pressed", "true")
else
pinLabelElem.html(gettext("Pinned"))
pinElem.removeAttr("data-tooltip")
pinElem.removeAttr("aria-pressed")
else
# If not pinned and not able to pin, pin is not shown
pinElem.removeClass("pinned")
pinElem.addClass("notpinned")
pinLabelElem.html(gettext("Pin Thread"))
pinElem.removeAttr("data-tooltip")
pinElem.attr("aria-pressed", "false")
updateModelDetails: =>
@renderVote()
@renderFlagged()
@renderPinned()
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
......@@ -114,64 +40,6 @@ if Backbone?
_delete: (event) ->
@trigger "thread:_delete", event
togglePin: (event) =>
event.preventDefault()
if @model.get('pinned')
@unPin()
else
@pin()
pin: =>
url = @model.urlFor("pinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', true)
error: =>
DiscussionUtil.discussionAlert("Sorry", "We had some trouble pinning this thread. Please try again.")
unPin: =>
url = @model.urlFor("unPinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', false)
error: =>
DiscussionUtil.discussionAlert("Sorry", "We had some trouble unpinning this thread. Please try again.")
toggleClosed: (event) ->
$elem = $(event.target)
url = @model.urlFor('close')
closed = @model.get('closed')
data = { closed: not closed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('closed', not closed)
@model.set('ability', response.ability)
toggleEndorse: (event) ->
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('endorsed', not endorsed)
highlight: (el) ->
if el.html()
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
......@@ -52,6 +52,12 @@ if Backbone?
else # mode == "inline"
@collapse()
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
closed: (closed) ->
@$(".discussion-reply-new").toggle(not closed)
@renderAddResponseButton()
})
expand: (event) ->
if event
event.preventDefault()
......@@ -200,8 +206,8 @@ if Backbone?
@$el.find(listSelector).append(view.el)
view.afterInsert()
renderAddResponseButton: ->
if @model.hasResponses() and @model.can('can_reply')
renderAddResponseButton: =>
if @model.hasResponses() and @model.can('can_reply') and !@model.get('closed')
@$el.find('div.add-response').show()
else
@$el.find('div.add-response').hide()
......@@ -215,9 +221,8 @@ if Backbone?
addComment: =>
@model.comment()
endorseThread: (endorsed) =>
is_endorsed = @$el.find(".is-endorsed").length > 0
@model.set 'endorsed', is_endorsed
endorseThread: =>
@model.set 'endorsed', @$el.find(".action-answer.is-checked").length > 0
submitComment: (event) ->
event.preventDefault()
......
if Backbone?
class @ResponseCommentShowView extends DiscussionContentView
events:
"click .action-delete":
(event) -> @_delete(event)
"keydown .action-delete":
(event) -> DiscussionUtil.activateOnSpace(event, @_delete)
"click .action-edit":
(event) -> @edit(event)
"keydown .action-edit":
(event) -> DiscussionUtil.activateOnSpace(event, @edit)
class @ResponseCommentShowView extends DiscussionContentShowView
tagName: "li"
initialize: ->
super()
@model.on "change", @updateModelDetails
abilityRenderer:
can_delete:
enable: -> @$(".action-delete").show()
disable: -> @$(".action-delete").hide()
editable:
enable: -> @$(".action-edit").show()
disable: -> @$(".action-edit").hide()
render: ->
@template = _.template($("#response-comment-show-template").html())
params = @model.toJSON()
@$el.html(
@template(
_.extend(
{
cid: @model.cid,
author_display: @getAuthorDisplay()
},
@model.attributes
)
)
)
@$el.html(@template(params))
@delegateEvents()
@renderAttrs()
@renderFlagged()
@markAsStaff()
@$el.find(".timeago").timeago()
@convertMath()
@addReplyLink()
......@@ -51,31 +35,8 @@ if Backbone?
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
markAsStaff: ->
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="staff-label">' + gettext('staff') + '</span>')
else if DiscussionUtil.isTA(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="community-ta-label">' + gettext('Community TA') + '</span>')
_delete: (event) =>
@trigger "comment:_delete", event
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Misuse Reported, click to remove report"))
@$(".discussion-flag-abuse .flag-label").html(gettext("Misuse Reported, click to remove report"))
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Report Misuse"))
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
updateModelDetails: =>
@renderFlagged()
edit: (event) =>
@trigger "comment:edit", event
if Backbone?
class @ThreadResponseShowView extends DiscussionContentView
events:
"click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnSpace(event, @toggleVote)
"click .action-endorse": "toggleEndorse"
"click .action-delete": "_delete"
"click .action-edit": "edit"
"click .discussion-flag-abuse": "toggleFlagAbuse"
"keydown .discussion-flag-abuse":
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
endorsed: (endorsed) ->
$endorseButton = @$(".action-endorse")
$endorseButton.toggleClass("is-clickable", @model.canBeEndorsed())
$endorseButton.toggleClass("is-endorsed", endorsed)
$endorseButton.toggle(endorsed || @model.canBeEndorsed())
})
$: (selector) ->
@$el.find(selector)
class @ThreadResponseShowView extends DiscussionContentShowView
initialize: ->
super()
@listenTo(@model, "change", @render)
renderTemplate: ->
@template = _.template($("#thread-response-show-template").html())
@template(@model.toJSON())
context = _.extend(
{
cid: @model.cid,
author_display: @getAuthorDisplay(),
endorser_display: @getEndorserDisplay()
},
@model.attributes
)
@template(context)
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderVote()
@renderAttrs()
@renderFlagged()
@$el.find(".posted-details .timeago").timeago()
@convertMath()
@markAsStaff()
@
convertMath: ->
......@@ -47,58 +29,8 @@ if Backbone?
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
markAsStaff: ->
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.addClass("staff")
@$el.prepend('<div class="staff-banner">' + gettext('staff') + '</div>')
else if DiscussionUtil.isTA(@model.get("user_id"))
@$el.addClass("community-ta")
@$el.prepend('<div class="community-ta-banner">' + gettext('Community TA') + '</div>')
edit: (event) ->
@trigger "response:edit", event
_delete: (event) ->
@trigger "response:_delete", event
toggleEndorse: (event) ->
event.preventDefault()
if not @model.canBeEndorsed()
return
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
new_endorsed = not endorsed
data = { endorsed: new_endorsed }
endorsement = {
"username": window.user.get("username"),
"time": new Date().toISOString()
}
@model.set(
"endorsed": new_endorsed
"endorsement": if new_endorsed then endorsement else null
)
@trigger "comment:endorse", not endorsed
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Misuse Reported, click to remove report"))
###
Translators: The text between start_sr_span and end_span is not shown
in most browsers but will be read by screen readers.
###
@$(".discussion-flag-abuse .flag-label").html(interpolate(gettext("Misuse Reported%(start_sr_span)s, click to remove report%(end_span)s"), {"start_sr_span": "<span class='sr'>", "end_span": "</span>"}, true))
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
from contextlib import contextmanager
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise
......@@ -39,6 +41,25 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
query = self._find_within(selector)
return query.present and query.visible
@contextmanager
def _secondary_action_menu_open(self, ancestor_selector):
"""
Given the selector for an ancestor of a secondary menu, return a context
manager that will open and close the menu
"""
self._find_within(ancestor_selector + " .action-more").click()
EmptyPromise(
lambda: self._is_element_visible(ancestor_selector + " .actions-dropdown"),
"Secondary action menu opened"
).fulfill()
yield
if self._is_element_visible(ancestor_selector + " .actions-dropdown"):
self._find_within(ancestor_selector + " .action-more").click()
EmptyPromise(
lambda: not self._is_element_visible(ancestor_selector + " .actions-dropdown"),
"Secondary action menu closed"
).fulfill()
def get_response_total_text(self):
"""Returns the response count text, or None if not present"""
return self._get_element_text(".response-count")
......@@ -89,11 +110,12 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
def start_response_edit(self, response_id):
"""Click the edit button for the response, loading the editing view"""
self._find_within(".response_{} .discussion-response .action-edit".format(response_id)).first.click()
EmptyPromise(
lambda: self.is_response_editor_visible(response_id),
"Response edit started"
).fulfill()
with self._secondary_action_menu_open(".response_{} .discussion-response".format(response_id)):
self._find_within(".response_{} .discussion-response .action-edit".format(response_id)).first.click()
EmptyPromise(
lambda: self.is_response_editor_visible(response_id),
"Response edit started"
).fulfill()
def is_show_comments_visible(self, response_id):
"""Returns true if the "show comments" link is visible for a response"""
......@@ -120,11 +142,13 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
def is_comment_deletable(self, comment_id):
"""Returns true if the delete comment button is present, false otherwise"""
return self._is_element_visible("#comment_{} div.action-delete".format(comment_id))
with self._secondary_action_menu_open("#comment_{}".format(comment_id)):
return self._is_element_visible("#comment_{} .action-delete".format(comment_id))
def delete_comment(self, comment_id):
with self.handle_alert():
self._find_within("#comment_{} div.action-delete".format(comment_id)).first.click()
with self._secondary_action_menu_open("#comment_{}".format(comment_id)):
self._find_within("#comment_{} .action-delete".format(comment_id)).first.click()
EmptyPromise(
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
......@@ -132,7 +156,8 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
def is_comment_editable(self, comment_id):
"""Returns true if the edit comment button is present, false otherwise"""
return self._is_element_visible("#comment_{} .action-edit".format(comment_id))
with self._secondary_action_menu_open("#comment_{}".format(comment_id)):
return self._is_element_visible("#comment_{} .action-edit".format(comment_id))
def is_comment_editor_visible(self, comment_id):
"""Returns true if the comment editor is present, false otherwise"""
......@@ -144,15 +169,16 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
def start_comment_edit(self, comment_id):
"""Click the edit button for the comment, loading the editing view"""
old_body = self.get_comment_body(comment_id)
self._find_within("#comment_{} .action-edit".format(comment_id)).first.click()
EmptyPromise(
lambda: (
self.is_comment_editor_visible(comment_id) and
not self.is_comment_visible(comment_id) and
self._get_comment_editor_value(comment_id) == old_body
),
"Comment edit started"
).fulfill()
with self._secondary_action_menu_open("#comment_{}".format(comment_id)):
self._find_within("#comment_{} .action-edit".format(comment_id)).first.click()
EmptyPromise(
lambda: (
self.is_comment_editor_visible(comment_id) and
not self.is_comment_visible(comment_id) and
self._get_comment_editor_value(comment_id) == old_body
),
"Comment edit started"
).fulfill()
def set_comment_editor_value(self, comment_id, new_body):
"""Replace the contents of the comment editor"""
......
......@@ -51,10 +51,12 @@
@import "discussion/utilities/variables";
@import "discussion/mixins";
@import 'discussion/discussion'; // Process old file after definitions but before everything else
@import "discussion/views/new-post";
@import "discussion/elements/actions";
@import "discussion/elements/editor";
@import "discussion/elements/labels";
@import "discussion/elements/navigation";
@import "discussion/views/thread";
@import "discussion/views/new-post";
@import "discussion/views/response";
@import 'discussion/utilities/developer';
@import 'discussion/utilities/shame';
......
......@@ -42,6 +42,9 @@ $very-light-text: #fff;
// ====================
// COLORS - utility
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
// COLORS
$black: rgb(0,0,0);
$black-t0: rgba($black, 0.125);
......
// forums - main app styling
// ====================
body.discussion {
.course-tabs .right {
......@@ -423,7 +422,7 @@ body.discussion {
}
h1 {
margin-bottom: $baseline/2;
margin-bottom: ($baseline/4);
font-size: 28px;
font-weight: 700;
letter-spacing: 0;
......@@ -432,18 +431,14 @@ body.discussion {
.posted-details {
font-size: 12px;
font-style: italic;
color: #888;
.username {
display: block;
font-size: 16px;
font-weight: 700;
}
.timeago, .top-post-status {
color: inherit;
font-style: italic;
}
}
......@@ -456,37 +451,6 @@ body.discussion {
p + p {
margin-top: $baseline;
}
.dogear {
display: block;
position: absolute;
top: -1px;
right: -1px;
width: 52px;
height: 51px;
background: url(../images/follow-dog-ear.png) 0 -52px no-repeat;
@include transition(none);
&.is-followed {
background-position: 0 0;
}
}
}
.discussion-post {
padding: ($baseline*2) ($baseline*2) 0 ($baseline*2);
> header .vote-btn {
position: relative;
z-index: 100;
margin-top: ($baseline/4);
margin-left: ($baseline*2);
}
.post-tools {
@include clearfix;
margin-top: 15px;
}
}
.discussion-post header,
......@@ -565,7 +529,7 @@ body.discussion {
.discussion-response {
@include box-sizing(border-box);
border-radius: 3px 3px 0 0;
padding: $baseline $baseline 0;
padding: $baseline;
background-color: $white;
}
.posted-by {
......@@ -594,94 +558,6 @@ body.discussion {
}
}
.vote-btn {
position: relative;
z-index: 100;
float: right;
display: block;
height: 27px;
padding: 0 8px;
border-radius: 5px;
border: 1px solid #b2b2b2;
@include linear-gradient(top, $white 35%, #ebebeb);
box-shadow: 0 1px 1px rgba(0, 0, 0, .15);
font-size: 12px;
font-weight: 700;
line-height: 25px;
color: #333;
.plus-icon {
display: inline-block;
width: 10px;
height: 10px;
margin: 8px 6px 0 0;
background: url(../images/vote-plus-icon.png) no-repeat;
font-size: 18px;
text-indent: -9999px;
color: #17b429;
overflow: hidden;
}
&.is-cast {
border-color: #379a42;
@include linear-gradient(top, #50cc5e, #3db84b);
color: $white;
text-shadow: 0 1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, .4) inset, 0 1px 2px $shadow;
.plus-icon {
background-position: 0 -10px;
color: #336a39;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
}
}
}
.endorse-btn {
display: block;
float: right;
width: 27px;
height: 27px;
margin-right: ($baseline/2);
border-radius: 27px;
border: 1px solid #a0a0a0;
@include linear-gradient(top, $white 35%, $gray-l4);
box-shadow: 0 1px 1px $shadow-l1;
cursor: default;
&.is-clickable {
cursor: auto;
}
.check-icon {
display: block;
width: 13px;
height: 12px;
margin: 8px auto;
background: url(../images/endorse-icon.png) no-repeat;
pointer-events: none;
}
&.mark-answer .check-icon {
background: url(../images/answer-icon.png) no-repeat;
}
&.is-endorsed {
border: 1px solid #4697c1;
@include linear-gradient(top, #6dccf1, #38a8e5);
box-shadow: 0 1px 1px $shadow-l1, 0 1px 0 rgba(255, 255, 255, .4) inset;
.check-icon {
background-position: 0 -12px;
}
&.mark-answer {
@include linear-gradient(top, tint(#1d9348, 60%), tint(#1d9348, 20%));
border: 1px solid #1d9348;
}
}
}
blockquote {
background: $gray-l5;
border-radius: 3px;
......@@ -689,89 +565,6 @@ body.discussion {
font-size: 14px;
}
.comments {
margin: 0;
border-radius: 0 0 3px 3px;
padding: 0;
background: $gray-l6;
box-shadow: 0 1px 3px -1px $shadow inset;
list-style: none;
> li {
border-top: 1px solid $gray-l4;
padding: ($baseline/2) $baseline;
}
blockquote {
background: $gray-l4;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
font-size: 14px;
}
.comment-form {
@include clearfix;
.comment-form-input {
padding: ($baseline/4) ($baseline/2);
background-color: $white;
font-size: 14px;
}
.discussion-submit-comment {
@include blue-button;
float: left;
margin-top: 8px;
}
.wmd-input {
height: 40px;
}
.discussion-errors {
margin: 0;
}
}
.response-body {
font-size: 13px;
margin-bottom: ($baseline/2);
p + p {
margin-top: 12px;
}
}
.posted-details {
font-size: 11px;
}
.staff-label {
margin-left: ($baseline/10);
padding: 0 ($baseline/5);
border-radius: 2px;
background: #009FE2;
font-size: 9px;
font-weight: 700;
font-style: normal;
color: white;
text-transform: uppercase;
}
}
.community-ta-label{
margin-left: ($baseline/10);
padding: 0 ($baseline/5);
border-radius: 2px;
background: $forum-color-community-ta;
font-size: 9px;
font-weight: 700;
font-style: normal;
color: white;
text-transform: uppercase;
}
.comment-form {
padding: ($baseline/2) 0;
......@@ -803,51 +596,6 @@ body.discussion {
}
}
.moderator-actions {
margin: 0;
padding: $baseline 0;
@include clearfix;
li {
float: left;
margin-right: ($baseline/2);
list-style: none;
}
a {
@include white-button;
height: 26px;
@include linear-gradient(top, $white 35%, #ebebeb);
font-size: 13px;
line-height: 24px;
color: #737373;
font-weight: normal;
box-shadow: 0 1px 1px $shadow-l1;
&:hover, &:focus {
@include linear-gradient(top, $white 35%, #ddd);
}
.delete-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 8px 4px 0 0;
background: url(../images/moderator-delete-icon.png) no-repeat;
}
.edit-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 7px 4px 0 0;
background: url(../images/moderator-edit-icon.png) no-repeat;
}
}
}
.main-article.new {
display: none;
padding: ($baseline*2.5);
......@@ -900,16 +648,6 @@ body.discussion {
// ====================
// post actions -global
.global-discussion-actions {
height: 60px;
@include linear-gradient(top, #ebebeb, #d9d9d9);
border-radius: 0 3px 0 0;
border-bottom: 1px solid #bcbcbc;
}
// ====================
// inline discussion module and profile thread styling
.discussion-module {
@extend .discussion-body;
......@@ -993,16 +731,6 @@ body.discussion {
margin-bottom: $baseline;
@include transition(all .25s linear 0s);
.dogear {
display: none;
}
&.expanded {
.dogear{
display: block;
}
}
p {
margin-bottom: 0;
}
......@@ -1174,10 +902,6 @@ body.discussion {
color: $white;
}
.moderator-actions {
padding-left: 0 !important;
}
section.pagination {
margin-top: 30px;
......@@ -1260,99 +984,6 @@ body.discussion {
}
}
// post actions - pinning
.discussion-pin {
font-size: 12px;
float:right;
padding-right: 5px;
font-style: italic;
margin-right: $baseline/2;
opacity: 0.8;
&.admin-pin {
cursor: pointer;
&:hover, &:focus {
@include transition(opacity .2s linear 0s);
opacity: 1.0;
}
}
}
.discussion-pin-inline {
font-size: 12px;
float:right;
font-style: italic;
position: relative;
right:-20px;
top:-13px;
margin-right:35px;
margin-top:13px;
opacity: 1.0;
}
.notpinned .icon {
display: block;
float: left;
margin: 3px;
width: 10px;
height: 14px;
padding-right: 3px;
color: #333;
}
.pinned .icon {
display: block;
float: left;
margin: 3px;
width: 10px;
height: 14px;
padding-right: 3px;
color: $pink;
}
.pinned span {
color: $pink;
font-style: italic;
}
.notpinned span {
color: #333;
font-style: italic;
}
.pinned-false
{
display:none;
}
// ====================
// post actions - flagging
.discussion-flag-abuse, .discussion-delete-comment, .discussion-edit-comment {
font-size: 12px;
float:right;
margin-left: ($baseline/2);
font-style: italic;
cursor:pointer;
color: $dark-gray;
opacity: 0.8;
&:hover, &:focus {
@include transition(opacity .2s linear 0s);
opacity: 1.0;
}
.flag-label {
font-style: italic;
margin-left: ($baseline/4);
}
}
.flagged * {
color: $pink;
}
// ====================
// post pagination
......
......@@ -113,4 +113,44 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
\ No newline at end of file
}
@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;
text-transform: uppercase;
white-space: nowrap;
border-color: $color;
color: $color;
.icon {
margin-right: ($baseline/5);
}
&:last-child {
margin-right: 0;
}
&.is-hidden {
display: none;
}
}
@mixin forum-user-label($color) {
@include font-size(9);
@extend %t-weight5;
vertical-align: middle;
margin-left: ($baseline/4);
border-radius: 2px;
padding: 0 ($baseline/5);
background: $color;
font-style: normal;
text-transform: uppercase;
color: white;
}
.discussion.container, .discussion-module {
// discussion - elements - actions
// ====================
// UI: general action list
.post-actions-list,
.response-actions-list,
.comment-actions-list {
@extend %ui-no-list;
text-align: right;
.actions-item {
@include box-sizing(border-box);
display: block;
margin: ($baseline/4) 0;
&.is-hidden {
display: none;
}
}
.more-wrapper {
position: relative;
}
}
// ====================
// UI: general actions dropdown layout
.actions-dropdown {
@extend %ui-no-list;
@extend %ui-depth1;
display: none;
position: absolute;
top: 100%;
right: 0;
pointer-events: none;
min-width: ($baseline*6.5);
&.is-expanded {
display: block;
pointer-events: auto;
}
.actions-dropdown-list {
@include box-sizing(border-box);
box-shadow: 0 1px 1px $shadow-l1;
position: relative;
width: 100%;
border-radius: 3px;
margin: 5px 0 0 0;
border: 1px solid $gray-l3;
padding: ($baseline/2) ($baseline*0.75);
background: $white;
// ui triangle/nub
&:after,
&:before {
bottom: 100%;
right: 3px;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-color: $transparent;
border-bottom-color: $white;
border-width: 6px;
margin-right: 1px;
}
&:before {
border-color: $transparent;
border-bottom-color: $gray-l3;
border-width: 7px;
}
}
.actions-item {
display: block;
margin: 0;
&.is-hidden {
display: none;
}
}
}
// ====================
// UI: general action
.action-button {
@include transition(border .5s linear 0s);
@include box-sizing(border-box);
display: inline-block;
border: 1px solid transparent;
border-radius: 5px;
color: $gray-l1;
.action-icon {
@extend %t-icon7;
display: inline-block;
height: $baseline;
width: $baseline;
border: 1px solid $gray-l3;
border-radius: 3px;
text-align: center;
color: $gray-l1;
.icon {
vertical-align: middle;
}
}
.action-label {
@extend %t-copy-sub2;
display: inline-block;
vertical-align: middle;
padding: 0 8px;
color: $gray-l1;
opacity: 0;
}
&:hover, &:focus {
.action-label {
opacity: 1;
}
.action-icon {
border-radius: 0 3px 3px 0;
}
}
// specific button styles
&.action-follow {
.action-label {
color: $blue-d1;
}
&.is-checked, &:hover, &:focus {
.action-icon {
background-color: $forum-color-following;
border: 1px solid $blue-d1;
color: $white;
}
}
&:hover, &:focus {
border-color: $forum-color-following;
}
}
&.action-vote {
.action-label {
opacity: 1;
}
&.is-checked, &:hover, &:focus {
.action-icon {
background-color: $green-d1;
border: 1px solid $green-d2;
color: $white;
}
}
&:hover, &:focus {
border-color: $green-d2;
.action-label {
color: $green-d2;
}
}
}
&.action-endorse {
&.is-checked, &:hover, &:focus {
.action-icon {
background-color: $blue-d1;
border: 1px solid $blue-d2;
color: $white;
}
}
&:hover, &:focus {
border-color: $blue-d2;
.action-label {
color: $blue-d2;
}
}
}
&.action-answer {
&.is-checked, &:hover, &:focus {
.action-icon {
border: 1px solid $green-d1;
background-color: $green-d1;
color: $white;
}
}
&:hover, &:focus {
border-color: $green-d1;
.action-label {
color: $green-d2;
}
}
}
// more drop-down menu
&.action-more {
position: relative;
&:hover, &:focus {
border-color: $gray;
.action-icon {
border: 1px solid $gray;
background-color: $gray;
color: $white;
}
.action-label {
opacity: 1;
color: $black;
}
}
}
}
// ====================
.actions-dropdown {
// UI: secondary action
.action-list-item {
@extend %t-copy-sub2;
display: block;
padding: ($baseline/10) 0;
white-space: nowrap;
text-align: right;
color: $gray-l1;
&:hover, &:focus {
color: $link-color;
}
.action-icon {
display: inline-block;
width: ($baseline/2);
margin-left: ($baseline/4);
color: inherit;
}
.action-label {
display: inline-block;
color: inherit;
}
// CASE: checked
&.is-checked {
// CASE: pin action
&.action-pin {
color: $pink;
}
// CASE: report action
&.action-report {
color: $pink;
}
// CASE: hover for any action
&:hover, &:focus {
color: $link-color;
}
}
}
}
.action-button, .action-list-item {
.action-label {
.label-checked {
display: none;
}
}
&.is-checked {
.label-unchecked {
display: none;
}
.label-checked {
display: inline;
}
}
}
}
// discussion - elements - labels
// ====================
body.discussion, .discussion-module {
.post-label-pinned {
@include forum-post-label($forum-color-pinned);
}
.post-label-following {
@include forum-post-label($forum-color-following);
}
.post-label-reported {
@include forum-post-label($forum-color-reported);
}
.post-label-closed {
@include forum-post-label($forum-color-closed);
}
.post-label-by-staff {
@include forum-post-label($forum-color-staff);
}
.post-label-by-community-ta {
@include forum-post-label($forum-color-community-ta);
}
.user-label-staff {
@include forum-user-label($forum-color-staff);
}
.user-label-community-ta {
@include forum-user-label($forum-color-community-ta);
}
}
\ No newline at end of file
......@@ -230,51 +230,6 @@
display: block;
}
%forum-nav-thread-label {
@extend %t-weight4;
@include font-size(9);
display: inline;
margin-top: ($baseline/4);
border: 1px solid;
border-radius: 3px;
padding: 1px 6px;
text-transform: uppercase;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
.icon {
margin-right: ($baseline/5);
}
}
.forum-nav-thread-label-pinned {
@extend %forum-nav-thread-label;
border-color: $forum-color-pinned;
color: $forum-color-pinned;
}
.forum-nav-thread-label-following {
@extend %forum-nav-thread-label;
border-color: $forum-color-following;
color: $forum-color-following;
}
.forum-nav-thread-label-staff {
@extend %forum-nav-thread-label;
border-color: $forum-color-staff;
color: $forum-color-staff;
}
.forum-nav-thread-label-community-ta {
@extend %forum-nav-thread-label;
border-color: $forum-color-community-ta;
color: $forum-color-community-ta;
}
%forum-nav-thread-wrapper-2-content {
@include font-size(11);
display: inline-block;
......
......@@ -136,3 +136,21 @@ li[class*=forum-nav-thread-label-] {
line-height: 14px;
}
}
// -------
// Actions
// -------
.discussion.container, .discussion-module {
// Override courseware
.post-actions-list, .response-actions-list, .comment-actions-list {
@extend %t-copy-sub2;
padding-left: 0 !important;
}
// Override global span
.action-label span, .action-icon span {
color: inherit;
}
}
$forum-color-active-thread: tint($blue, 85%);
$forum-color-pinned: $pink;
$forum-color-reported: $pink;
$forum-color-closed: $black;
$forum-color-following: $blue;
$forum-color-staff: $blue;
$forum-color-community-ta: $green-d1;
......
// discussion - thread layout
// ====================
// general thread layout
body.discussion, .discussion-module {
// post layout
.discussion-post {
padding: ($baseline*2) ($baseline*2) $baseline ($baseline*2);
border-radius: 3px 3px 0 0;
background-color: $white;
.post-header-content {
display: inline-block;
width: flex-grid(9,12);
}
.post-header-actions {
display: inline-block;
float: right;
vertical-align: middle;
width: flex-grid(3,12);
}
}
// response layout
.discussion-response {
min-height: ($baseline*7.5);
.username {
@include font-size(14);
@extend %t-weight5;
}
.response-header-content {
display: inline-block;
vertical-align: top;
width: flex-grid(9,12);
}
.response-header-actions {
width: flex-grid(3,12);
float: right;
}
}
// comments layout
.comments {
@extend %ui-no-list;
border-radius: 0 0 3px 3px;
background: $gray-l6;
box-shadow: 0 1px 3px -1px $shadow inset;
> li {
border-top: 1px solid $gray-l4;
padding: ($baseline/2) $baseline;
}
blockquote {
background: $gray-l4;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
font-size: 14px;
}
.comment-form {
@include clearfix;
.comment-form-input {
padding: ($baseline/4) ($baseline/2);
background-color: $white;
font-size: 14px;
}
.discussion-submit-comment {
@include blue-button;
float: left;
margin-top: 8px;
}
.wmd-input {
height: 40px;
}
.discussion-errors {
margin: 0;
}
}
.response-body {
display: inline-block;
margin-bottom: ($baseline/2);
width: flex-grid(10,12);
font-size: 13px;
p + p {
margin-top: 12px;
}
}
.comment-actions-list {
display: inline-block;
width: flex-grid(2,12);
vertical-align: top;
}
//TO-DO : clean up posted-details styling, currently reused by responses and comments
.posted-details {
margin-top: 0;
}
}
}
.forum-thread-main-wrapper {
border-bottom: 1px solid $white; // Prevent collapsing margins
border-radius: 3px 3px 0 0;
......@@ -6,7 +120,7 @@
body.discussion, .discussion-thread.expanded {
.forum-thread-main-wrapper {
margin-bottom: $baseline;
box-shadow: 0 1px 3px $shadow;
}
}
<%! from django.utils.translation import ugettext as _ %>
<%! from django.template.defaultfilters import escapejs %>
<%! from django_comment_client.permissions import has_permission %>
## IMPORTANT: In order to keep js tests valid and relevant, please be sure to update the appropriate HTML in
## common/static/coffee/spec/discussion_spec_helper.coffee is changed and regenerated, whenever this one changes.
<script aria-hidden="true" type="text/template" id="thread-template">
<article class="discussion-article" data-id="${'<%- id %>'}">
<div class="thread-wrapper">
......@@ -50,48 +51,43 @@
<div class="group-visibility-label">${"<%- obj.group_string%>"}</div>
${"<% } %>"}
<a href="#" class="vote-btn" role="button" aria-pressed="false"><!-- DiscussionContentView.renderVote populates this --></a>
<h1>${'<%- title %>'}</h1>
<p class="posted-details">
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="username">${'<%- username %>'}</a>
${"<% } else { %>"}
${_('anonymous') | h}
${"<% } %>"}
## This part is incredibly gross but necessary to combine i18n in
## mako with logic in underscore.
## Translators: post_type describes the kind of post this is
## (e.g. "question" or "discussion"); time_ago is how much time
## has passed since the post was created (e.g. "4 hours ago")
${"{post_type} posted {time_ago}".format(
post_type="<%- thread_type %>",
time_ago="<span class='timeago' title='<%- created_at %>'><%- created_at %></span>"
)}
<div class="post-header-content">
<span class="post-status-closed top-post-status" style="display: none">
${_("&bull; This thread is closed.")}
</span>
</p>
<a href="javascript:void(0)" class="dogear action-follow" data-tooltip="${_('follow') | h}" role="checkbox" aria-checked="false">
<span class="sr">${_("Follow this post")}</span>
</a>
<h1>${'<%- title %>'}</h1>
<p class="posted-details">
## This part is incredibly gross but necessary to combine i18n in
## mako with logic in underscore.
## Translators: post_type describes the kind of post this is
## (e.g. "question" or "discussion"); time_ago is how much time
## has passed since the post was created (e.g. "4 hours ago")
${_("{post_type} posted {time_ago} by {author}").format(
post_type="<%- thread_type %>",
time_ago="<span class='timeago' title='<%- created_at %>'><%- created_at %></span>",
author="<%= author_display %>"
)}
</p>
<div class="post-labels">
<span class="post-label-pinned"><i class="icon icon-pushpin"></i>${_("Pinned")}</span>
<span class="post-label-reported"><i class="icon icon-flag"></i>${_("Reported")}</span>
<span class="post-label-closed"><i class="icon icon-lock"></i>${_("Closed")}</span>
</div>
</div>
<div class="post-header-actions post-extended-content">
${"""<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'post',
primaryActions: ['vote', 'follow'],
secondaryActions: ['pin', 'edit', 'delete', 'report', 'close']
}
)
%>"""}
</div>
</header>
<div class="post-body">${'<%- body %>'}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-flag"></i><span class="flag-label">${_("Report Misuse")}</span></div>
% if course and has_permission(user, 'openclose_thread', course.id):
<div class="admin-pin discussion-pin notpinned" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-pushpin"></i><span class="pin-label">${_("Pin Thread")}</span></div>
%else:
${"<% if (pinned) { %>"}
<div class="discussion-pin notpinned">
<i class="icon icon-pushpin"></i><span class="pin-label">${_("Pinned")}</span></div>
${"<% } %>"}
% endif
<% js_block = u"""
var courseware_link = interpolate('<a href="%s">%s</a>', [courseware_url, _.escape(courseware_title)]);
......@@ -104,14 +100,6 @@
${'<% if (mode == "tab" && obj.courseware_url) { %>'}
<div class="post-context">${'<%'}${js_block}${'%>'}</div>
${'<% } %>'}
<div class="post-extended-content">
<ul class="moderator-actions">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> ${_("Edit")}</a></li>
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> ${_("Delete")}</a></li>
<li style="display: none"><a class="action-openclose" href="javascript:void(0)"><span class="edit-icon"></span> ${_("Close")}</a></li>
</ul>
</div>
</div>
</script>
......@@ -156,57 +144,58 @@
<script aria-hidden="true" type="text/template" id="thread-response-show-template">
<header>
<a href="#" class="vote-btn" role="button" aria-pressed="false"><!-- DiscussionContentView.renderVote() populates this --></a>
<%
tooltip_expr = u"<%- thread.get('thread_type') == 'question' ? '{mark_answer}' : '{endorse}' %>".format(
endorse=_("endorse"),
mark_answer=_("mark as answer")
)
%>
<a
href="javascript:void(0)"
class="endorse-btn action-endorse ${"<%= thread.get('thread_type') == 'question' ? 'mark-answer' : '' %>"}"
data-tooltip="${tooltip_expr}"
>
<span class="check-icon"></span>
</a>
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>
${"<% } else { %>"}
<span class="anonymous"><em>${_('anonymous')}</em></span>
${"<% } %>"}
<div class="response-header-content">
${'<%= author_display %>'}
<p class="posted-details">
<span class="timeago" title="${'<%= created_at %>'}">${'<%= created_at %>'}</span>
<%
js_block = u"""
interpolate(
endorsement.username ? "{user_fmt_str}" : "{anon_fmt_str}",
thread.get("thread_type") == "question" ?
(endorsement.username ? "{question_user_fmt_str}" : "{question_anon_fmt_str}") :
(endorsement.username ? "{discussion_user_fmt_str}" : "{discussion_anon_fmt_str}"),
{{
'time_ago': '<span class="timeago" title="' + endorsement.time + '">' + endorsement.time + '</span>',
'user': endorsement.username
'user': endorser_display
}},
true
)""".format(
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
user_fmt_str=escapejs(_("marked as answer %(time_ago)s by %(user)s")),
question_user_fmt_str=escapejs(_("marked as answer %(time_ago)s by %(user)s")),
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
question_anon_fmt_str=escapejs(_("marked as answer %(time_ago)s")),
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
anon_fmt_str=escapejs(_("marked as answer %(time_ago)s")),
discussion_user_fmt_str=escapejs(_("endorsed %(time_ago)s by %(user)s")),
## Translators: time_ago is a placeholder for a fuzzy, relative timestamp
## like "4 hours ago" or "about a month ago"
discussion_anon_fmt_str=escapejs(_("endorsed %(time_ago)s")),
)
%>
${"<% if (thread.get('thread_type') == 'question' && obj.endorsement) { %> - <%="}${js_block}${"%><% } %>"}
</p>
${"<% if (obj.endorsement) { %> - <%="}${js_block}${"%><% } %>"}
</p>
<div class="post-labels">
<span class="post-label-reported"><i class="icon icon-flag"></i>${_("Reported")}</span>
</div>
</div>
<div class="response-header-actions">
${"""<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'response',
primaryActions: ['vote', thread.get('thread_type') == 'question' ? 'answer' : 'endorse'],
secondaryActions: ['edit', 'delete', 'report']
}
)
%>"""}
</div>
</header>
<div class="response-body">${"<%- body %>"}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-flag"></i><span class="flag-label">${_("Report Misuse")}</span></div>
</div>
<ul class="moderator-actions">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> ${_("Edit")}</a></li>
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> ${_("Delete")}</a></li>
<li style="display: none"><a class="action-openclose" href="javascript:void(0)"><span class="edit-icon"></span> ${_("Close")}</a></li>
</ul>
</script>
<script aria-hidden="true" type="text/template" id="thread-response-edit-template">
......@@ -224,31 +213,34 @@
<script aria-hidden="true" type="text/template" id="response-comment-show-template">
<div id="comment_${'<%- id %>'}">
<div class="response-body">${'<%- body %>'}</div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="${_('Report Misuse') | h}" role="button" aria-pressed="false" tabindex="0">
<i class="icon icon-flag"></i><span class="sr flag-label">${_("Report Misuse")}</span></div>
<div style="display: none" class="discussion-delete-comment action-delete" data-tooltip="${_('Delete Comment') | h}" role="button" tabindex="0">
<i class="icon icon-remove"></i><span class="sr">${_("Delete Comment")}</span></div>
<div class="discussion-edit-comment action-edit" data-tooltip="${_('Edit') | h}" role="button" tabindex="0">
<i class="icon icon-pencil"></i><span class="sr">${_("Edit")}</span></div>
<%
js_block = u"""
interpolate(
'{}',
{{'time_ago': '<span class=\"timeago\" title=\"' + created_at + '\">' + created_at + '</span>'}},
true
)""".format(
## Translators: 'timeago' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
escapejs(_('-posted %(time_ago)s by'))
)
%>
<p class="posted-details">
${'<%='}${js_block}${'%>'}
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="profile-link">${'<%- username %>'}</a>
${"<% } else { %>"}
${_('anonymous')}
${"<% } %>"}
${"""<%=
_.template(
$('#forum-actions').html(),
{
contentId: cid,
contentType: 'comment',
primaryActions: [],
secondaryActions: ['edit', 'delete', 'report']
}
)
%>"""}
<%
js_block = u"""
interpolate(
'{}',
{{'time_ago': '<span class=\"timeago\" title=\"' + created_at + '\">' + created_at + '</span>', 'author': author_display}},
true
)""".format(
## Translators: 'timeago' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
escapejs(_('posted %(time_ago)s by %(author)s'))
)
%>
<p class="posted-details">
${'<%='}${js_block}${'%>'}
</p>
<div class="post-labels">
<span class="post-label-reported"><i class="icon icon-flag"></i>${_("Reported")}</span>
</div>
</div>
</script>
......@@ -296,16 +288,16 @@
js_block = u"""
var labels = "";
if (pinned) {{
labels += '<li class="forum-nav-thread-label-pinned"><i class="icon icon-pushpin"></i>{pinned_text}</li> ';
labels += '<li class="post-label-pinned"><i class="icon icon-pushpin"></i>{pinned_text}</li> ';
}}
if (typeof(subscribed) != "undefined" && subscribed) {{
labels += '<li class="forum-nav-thread-label-following"><i class="icon icon-star"></i>{following_text}</li> ';
labels += '<li class="post-label-following"><i class="icon icon-star"></i>{following_text}</li> ';
}}
if (staff_authored) {{
labels += '<li class="forum-nav-thread-label-staff"><i class="icon icon-user"></i>{staff_text}</li> ';
labels += '<li class="post-label-by-staff"><i class="icon icon-user"></i>{staff_text}</li> ';
}}
if (community_ta_authored) {{
labels += '<li class="forum-nav-thread-label-community-ta"><i class="icon icon-user"></i>{community_ta_text}</li> ';
labels += '<li class="post-label-by-community-ta"><i class="icon icon-user"></i>{community_ta_text}</li> ';
}}
if (labels != "") {{
print('<ul class="forum-nav-thread-labels">' + labels + '</ul>');
......@@ -554,3 +546,107 @@
<ul role="menu" class="topic-submenu">${'<%= entries %>'}</ul>
</li>
</script>
<%def name="primaryAction(action_class, icon, sr_label, unchecked_label, checked_label)">
<script type="text/template" id="forum-action-${action_class}">
<li class="actions-item">
<a href="javascript:void(0)" class="action-button action-${action_class}" role="checkbox" aria-checked="false">
<span class="sr">${sr_label}</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">${unchecked_label}</span>
<span class="label-checked">${checked_label}</span>
</span>
<span class="action-icon"><i class="icon icon-${icon}"></i></span>
</a>
</li>
</script>
</%def>
${primaryAction("endorse", "ok", _("Endorse"), _("Endorse"), _("Unendorse"))}
${primaryAction("answer", "ok", _("Mark as Answer"), _("Mark as Answer"), _("Unmark as Answer"))}
${primaryAction("follow", "star", _("Follow"), _("Follow"), _("Unfollow"))}
<script type="text/template" id="forum-action-vote">
<li class="actions-item">
<a href="#" class="action-button action-vote" role="checkbox" aria-checked="false">
## Vote counts are populated by JS
<span class="sr">${_("Vote")}</span>
<span class="sr js-sr-vote-count"></span>
<span class="action-label" aria-hidden="true">
<span class="js-visual-vote-count"></span>
</span>
<span class="action-icon" aria-hidden="true">
<i class="icon icon-plus"></i>
</span>
</a>
</li>
</script>
<%def name="secondaryStateAction(action_class, icon, sr_label, unchecked_label, checked_label)">
<script type="text/template" id="forum-action-${action_class}">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-${action_class}" role="checkbox" aria-checked="false">
<span class="sr">${sr_label}</span>
<span class="action-label" aria-hidden="true">
<span class="label-unchecked">${unchecked_label}</span>
<span class="label-checked">${checked_label}</span>
</span>
<span class="action-icon">
<i class="icon icon-${icon}"></i>
</span>
</a>
</li>
</script>
</%def>
${secondaryStateAction("report", "flag", _("Report abuse"), _("Report"), _("Unreport"))}
${secondaryStateAction("pin", "pushpin", _("Pin"), _("Pin"), _("Unpin"))}
${secondaryStateAction("close", "lock", _("Close"), _("Close"), _("Open"))}
<%def name="secondaryAction(action_class, icon, label)">
<script type="text/template" id="forum-action-${action_class}">
<li class="actions-item">
<a href="javascript:void(0)" class="action-list-item action-${action_class}" role="button">
<span class="action-label">${label}</span>
<span class="action-icon"><i class="icon icon-${icon}"></i></span>
</a>
</li>
</script>
</%def>
${secondaryAction("edit", "pencil", _("Edit"))}
${secondaryAction("delete", "remove", _("Delete"))}
<script type="text/template" id="forum-actions">
<ul class="${"<%= contentType %>"}-actions-list">
${"<% _.each(primaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>"}
<li class="actions-item is-visible">
<div class="more-wrapper">
<a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-${"<%= contentId %>"}">
<span class="action-label">${_("More")}</span>
<span class="action-icon"><i class="icon icon-ellipsis-horizontal"></i></span>
</a>
<div class="actions-dropdown" id="action-menu-${"<%= contentType %>"}" aria-expanded="false">
<ul class="actions-dropdown-list">
${"<% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %>"}
</ul>
</div>
</div>
</li>
</ul>
</script>
<script aria-hidden="true" type="text/template" id="post-user-display-template">
${"<% if (username) { %>"}
<a href="${'<%- user_url %>'}" class="username">${'<%- username %>'}</a>
${"<% if (is_community_ta) { %>"}
<span class="user-label-community-ta">${_("Community TA")}</span>
${"<% } else if (is_staff) { %>"}
<span class="user-label-staff">${_("Staff")}</span>
${"<% } %>"}
${"<% } else { %>"}
${_('anonymous') | h}
${"<% } %>"}
</script>
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