Merge remote-tracking branch 'edx/master' into andya/merge-hotfix-2014-08-29
This commit is contained in:
@@ -77,32 +77,6 @@ def marketing_link_context_processor(request):
|
||||
)
|
||||
|
||||
|
||||
def header_footer_context_processor(request):
|
||||
"""
|
||||
A django context processor to pass feature flags through to all Django
|
||||
Templates that are related to the display of the header and footer in
|
||||
the edX platform.
|
||||
"""
|
||||
# TODO: ECOM-136 Remove this processor with the corresponding header and footer feature flags.
|
||||
return dict(
|
||||
[
|
||||
("ENABLE_NEW_EDX_HEADER", settings.FEATURES.get("ENABLE_NEW_EDX_HEADER", False)),
|
||||
("ENABLE_NEW_EDX_FOOTER", settings.FEATURES.get("ENABLE_NEW_EDX_FOOTER", False))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def open_source_footer_context_processor(request):
|
||||
"""
|
||||
Checks the site name to determine whether to use the edX.org footer or the Open Source Footer.
|
||||
"""
|
||||
return dict(
|
||||
[
|
||||
("IS_EDX_DOMAIN", settings.FEATURES.get('IS_EDX_DOMAIN', False))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
|
||||
# see if there is an override template defined in the microsite
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
from mock import patch, Mock
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
@@ -11,16 +10,11 @@ from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
import edxmako.middleware
|
||||
from edxmako import add_lookup, LOOKUP
|
||||
from edxmako.shortcuts import (
|
||||
marketing_link,
|
||||
render_to_string,
|
||||
header_footer_context_processor,
|
||||
open_source_footer_context_processor
|
||||
)
|
||||
from edxmako.shortcuts import marketing_link, render_to_string
|
||||
from student.tests.factories import UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
@ddt.ddt
|
||||
|
||||
class ShortcutsTests(UrlResetMixin, TestCase):
|
||||
"""
|
||||
Test the edxmako shortcuts file
|
||||
@@ -40,26 +34,6 @@ class ShortcutsTests(UrlResetMixin, TestCase):
|
||||
link = marketing_link('ABOUT')
|
||||
self.assertEquals(link, expected_link)
|
||||
|
||||
@ddt.data((True, True), (False, False), (False, True), (True, False))
|
||||
@ddt.unpack
|
||||
def test_header_and_footer(self, header_setting, footer_setting):
|
||||
with patch.dict('django.conf.settings.FEATURES', {
|
||||
'ENABLE_NEW_EDX_HEADER': header_setting,
|
||||
'ENABLE_NEW_EDX_FOOTER': footer_setting,
|
||||
}):
|
||||
result = header_footer_context_processor({})
|
||||
self.assertEquals(footer_setting, result.get('ENABLE_NEW_EDX_FOOTER'))
|
||||
self.assertEquals(header_setting, result.get('ENABLE_NEW_EDX_HEADER'))
|
||||
|
||||
@ddt.data(True, False)
|
||||
@ddt.unpack
|
||||
def test_edx_footer(self, expected_result):
|
||||
with patch.dict('django.conf.settings.FEATURES', {
|
||||
'IS_EDX_DOMAIN': expected_result
|
||||
}):
|
||||
result = open_source_footer_context_processor({})
|
||||
self.assertEquals(expected_result, result.get('IS_EDX_DOMAIN'))
|
||||
|
||||
|
||||
class AddLookupTests(TestCase):
|
||||
"""
|
||||
|
||||
@@ -46,7 +46,8 @@ from student.models import (
|
||||
Registration, UserProfile, PendingNameChange,
|
||||
PendingEmailChange, CourseEnrollment, unique_id_for_user,
|
||||
CourseEnrollmentAllowed, UserStanding, LoginFailures,
|
||||
create_comments_service_user, PasswordHistory, UserSignupSource
|
||||
create_comments_service_user, PasswordHistory, UserSignupSource,
|
||||
anonymous_id_for_user
|
||||
)
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
@@ -92,6 +93,9 @@ from util.password_policy_validators import (
|
||||
from third_party_auth import pipeline, provider
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
|
||||
import analytics
|
||||
from eventtracking import tracker
|
||||
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
@@ -381,6 +385,10 @@ def register_user(request, extra_context=None):
|
||||
'username': '',
|
||||
}
|
||||
|
||||
# We save this so, later on, we can determine what course motivated a user's signup
|
||||
# if they actually complete the registration process
|
||||
request.session['registration_course_id'] = context['course_id']
|
||||
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
@@ -951,6 +959,31 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
if LoginFailures.is_feature_enabled():
|
||||
LoginFailures.clear_lockout_counter(user)
|
||||
|
||||
# Track the user's sign in
|
||||
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.identify(anonymous_id_for_user(user, None), {
|
||||
'email': email,
|
||||
'username': username,
|
||||
})
|
||||
|
||||
# If the user entered the flow via a specific course page, we track that
|
||||
registration_course_id = request.session.get('registration_course_id')
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.authenticated",
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': registration_course_id
|
||||
},
|
||||
context={
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
request.session['registration_course_id'] = None
|
||||
|
||||
if user is not None and user.is_active:
|
||||
try:
|
||||
# We do not log here, because we have a handler registered
|
||||
@@ -1398,6 +1431,33 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
(user, profile, registration) = ret
|
||||
|
||||
dog_stats_api.increment("common.student.account_created")
|
||||
|
||||
email = post_vars['email']
|
||||
|
||||
# Track the user's registration
|
||||
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.identify(anonymous_id_for_user(user, None), {
|
||||
email: email,
|
||||
username: username,
|
||||
})
|
||||
|
||||
registration_course_id = request.session.get('registration_course_id')
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.registered",
|
||||
{
|
||||
"category": "conversion",
|
||||
"label": registration_course_id
|
||||
},
|
||||
context={
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
request.session['registration_course_id'] = None
|
||||
|
||||
create_comments_service_user(user)
|
||||
|
||||
context = {
|
||||
|
||||
@@ -4,7 +4,7 @@ Loaded by Django's settings mechanism. Consequently, this module must not
|
||||
invoke the Django armature.
|
||||
"""
|
||||
|
||||
from social.backends import google, linkedin
|
||||
from social.backends import google, linkedin, facebook
|
||||
|
||||
_DEFAULT_ICON_CLASS = 'icon-signin'
|
||||
|
||||
@@ -150,6 +150,26 @@ class LinkedInOauth2(BaseProvider):
|
||||
return provider_details.get('fullname')
|
||||
|
||||
|
||||
class FacebookOauth2(BaseProvider):
|
||||
"""Provider for LinkedIn's Oauth2 auth system."""
|
||||
|
||||
BACKEND_CLASS = facebook.FacebookOAuth2
|
||||
ICON_CLASS = 'icon-facebook'
|
||||
NAME = 'Facebook'
|
||||
SETTINGS = {
|
||||
'SOCIAL_AUTH_FACEBOOK_KEY': None,
|
||||
'SOCIAL_AUTH_FACEBOOK_SECRET': None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_email(cls, provider_details):
|
||||
return provider_details.get('email')
|
||||
|
||||
@classmethod
|
||||
def get_name(cls, provider_details):
|
||||
return provider_details.get('fullname')
|
||||
|
||||
|
||||
class Registry(object):
|
||||
"""Singleton registry of third-party auth providers.
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
def assert_register_response_before_pipeline_looks_correct(self, response):
|
||||
"""Asserts a GET of /register not in the pipeline looks correct."""
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn('Sign in with ' + self.PROVIDER_CLASS.NAME, response.content)
|
||||
self.assertIn('Sign up with ' + self.PROVIDER_CLASS.NAME, response.content)
|
||||
self.assert_signin_button_looks_functional(response.content, pipeline.AUTH_ENTRY_REGISTER)
|
||||
|
||||
def assert_signin_button_looks_functional(self, content, auth_entry):
|
||||
|
||||
@@ -41,11 +41,11 @@ describe 'All Content', ->
|
||||
|
||||
it 'can update info', ->
|
||||
@content.updateInfo {
|
||||
ability: 'can_endorse',
|
||||
ability: {'can_edit': true},
|
||||
voted: true,
|
||||
subscribed: true
|
||||
}
|
||||
expect(@content.get 'ability').toEqual 'can_endorse'
|
||||
expect(@content.get 'ability').toEqual {'can_edit': true}
|
||||
expect(@content.get 'voted').toEqual true
|
||||
expect(@content.get 'subscribed').toEqual true
|
||||
|
||||
@@ -77,3 +77,39 @@ describe 'All Content', ->
|
||||
myComments = new Comments
|
||||
myComments.add @comment1
|
||||
expect(myComments.find('123')).toBe @comment1
|
||||
|
||||
it 'can be endorsed', ->
|
||||
|
||||
DiscussionUtil.loadRoles(
|
||||
{"Moderator": [111], "Administrator": [222], "Community TA": [333]}
|
||||
)
|
||||
@discussionThread = new Thread({id: 1, thread_type: "discussion", user_id: 99})
|
||||
@discussionResponse = new Comment({id: 1, thread: @discussionThread})
|
||||
@questionThread = new Thread({id: 1, thread_type: "question", user_id: 99})
|
||||
@questionResponse = new Comment({id: 1, thread: @questionThread})
|
||||
|
||||
# mod
|
||||
window.user = new DiscussionUser({id: 111})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# admin
|
||||
window.user = new DiscussionUser({id: 222})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# TA
|
||||
window.user = new DiscussionUser({id: 333})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(true)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# thread author
|
||||
window.user = new DiscussionUser({id: 99})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(false)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(true)
|
||||
|
||||
# anyone else
|
||||
window.user = new DiscussionUser({id: 999})
|
||||
expect(@discussionResponse.canBeEndorsed()).toBe(false)
|
||||
expect(@questionResponse.canBeEndorsed()).toBe(false)
|
||||
|
||||
|
||||
@@ -3,4 +3,606 @@ class @DiscussionSpecHelper
|
||||
@setUpGlobals = ->
|
||||
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
|
||||
window.$$course_id = "edX/999/test"
|
||||
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
|
||||
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>
|
||||
""")
|
||||
|
||||
27
common/static/coffee/spec/discussion/utils_spec.coffee
Normal file
27
common/static/coffee/spec/discussion/utils_spec.coffee
Normal file
@@ -0,0 +1,27 @@
|
||||
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})
|
||||
@@ -1,26 +1,7 @@
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,23 @@ describe "DiscussionThreadListView", ->
|
||||
<script 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-1">
|
||||
<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 = "";
|
||||
@@ -27,9 +43,6 @@ describe "DiscussionThreadListView", ->
|
||||
}
|
||||
%>
|
||||
</div><div class="forum-nav-thread-wrapper-2">
|
||||
<% if (endorsed) { %>
|
||||
<span class="forum-nav-thread-endorsed"><i class="icon icon-ok"></i><span class="sr">Endorsed response</span></span>
|
||||
<% } %>
|
||||
<span class="forum-nav-thread-votes-count">+<%=
|
||||
interpolate(
|
||||
'%(votes_up_count)s%(span_sr_open)s votes %(span_close)s',
|
||||
@@ -84,9 +97,6 @@ describe "DiscussionThreadListView", ->
|
||||
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-all">
|
||||
<a href="#" class="forum-nav-browse-title">All Discussions</a>
|
||||
</li>
|
||||
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-flagged">
|
||||
<a href="#" class="forum-nav-browse-title"><i class="icon icon-flag"></i>Flagged Discussions</a>
|
||||
</li>
|
||||
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-following">
|
||||
<a href="#" class="forum-nav-browse-title"><i class="icon icon-star"></i>Posts I'm Following</a>
|
||||
</li>
|
||||
@@ -98,7 +108,7 @@ describe "DiscussionThreadListView", ->
|
||||
<ul class="forum-nav-browse-submenu">
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id='{"sort_key": null, "id": "child"}'
|
||||
data-discussion-id="child"
|
||||
data-cohorted="false"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Child</a>
|
||||
@@ -106,7 +116,7 @@ describe "DiscussionThreadListView", ->
|
||||
</ul>
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id='{"sort_key": null, "id": "sibling"}'
|
||||
data-discussion-id="sibling"
|
||||
data-cohorted="false"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Sibling</a>
|
||||
@@ -115,7 +125,7 @@ describe "DiscussionThreadListView", ->
|
||||
</li>
|
||||
<li
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id='{"sort_key": null, "id": "other"}'
|
||||
data-discussion-id="other"
|
||||
data-cohorted="false"
|
||||
>
|
||||
<a href="#" class="forum-nav-browse-title">Other Category</a>
|
||||
@@ -124,13 +134,21 @@ describe "DiscussionThreadListView", ->
|
||||
</div>
|
||||
<div class="forum-nav-thread-list-wrapper">
|
||||
<div class="forum-nav-refine-bar">
|
||||
<span class="forum-nav-sort">
|
||||
<label class="forum-nav-filter-main">
|
||||
<select class="forum-nav-filter-main-control">
|
||||
<option value="all">Show all</option>
|
||||
<option value="unread">Unread</option>
|
||||
<option value="unanswered">Unanswered</option>
|
||||
<option value="flagged">Flagged</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="forum-nav-sort">
|
||||
<select class="forum-nav-sort-control">
|
||||
<option value="date">by recent activity</option>
|
||||
<option value="comments">by most activity</option>
|
||||
<option value="votes">by most votes</option>
|
||||
</select>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-alerts"></div>
|
||||
@@ -150,21 +168,21 @@ describe "DiscussionThreadListView", ->
|
||||
<div class="forum-nav"></div>
|
||||
"""
|
||||
@threads = [
|
||||
makeThreadWithProps({
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "1",
|
||||
title: "Thread1",
|
||||
votes: {up_count: '20'},
|
||||
comments_count: 1,
|
||||
created_at: '2013-04-03T20:08:39Z',
|
||||
}),
|
||||
makeThreadWithProps({
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "2",
|
||||
title: "Thread2",
|
||||
votes: {up_count: '42'},
|
||||
comments_count: 2,
|
||||
created_at: '2013-04-03T20:07:39Z',
|
||||
}),
|
||||
makeThreadWithProps({
|
||||
DiscussionViewSpecHelper.makeThreadWithProps({
|
||||
id: "3",
|
||||
title: "Thread3",
|
||||
votes: {up_count: '12'},
|
||||
@@ -179,20 +197,8 @@ describe "DiscussionThreadListView", ->
|
||||
@view = new DiscussionThreadListView({collection: @discussion, el: $(".forum-nav")})
|
||||
@view.render()
|
||||
|
||||
makeThreadWithProps = (props) ->
|
||||
# Minimal set of properties necessary for rendering
|
||||
thread = {
|
||||
id: "dummy_id",
|
||||
pinned: false,
|
||||
endorsed: false,
|
||||
votes: {up_count: '0'},
|
||||
unread_comments_count: 0,
|
||||
comments_count: 0,
|
||||
}
|
||||
$.extend(thread, props)
|
||||
|
||||
renderSingleThreadWithProps = (props) ->
|
||||
makeView(new Discussion([new Thread(makeThreadWithProps(props))])).render()
|
||||
makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render()
|
||||
|
||||
makeView = (discussion) ->
|
||||
return new DiscussionThreadListView(
|
||||
@@ -200,6 +206,31 @@ describe "DiscussionThreadListView", ->
|
||||
collection: discussion
|
||||
)
|
||||
|
||||
expectFilter = (filterVal) ->
|
||||
$.ajax.andCallFake((params) ->
|
||||
_.each(["unread", "unanswered", "flagged"], (paramName)->
|
||||
if paramName == filterVal
|
||||
expect(params.data[paramName]).toEqual(true)
|
||||
else
|
||||
expect(params.data[paramName]).toBeUndefined()
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
|
||||
describe "should filter correctly", ->
|
||||
_.each(["all", "unread", "unanswered", "flagged"], (filterVal) ->
|
||||
it "for #{filterVal}", ->
|
||||
expectFilter(filterVal)
|
||||
@view.$(".forum-nav-filter-main-control").val(filterVal).change()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
)
|
||||
|
||||
it "search should clear filter", ->
|
||||
expectFilter(null)
|
||||
@view.$(".forum-nav-filter-main-control").val("flagged")
|
||||
@view.searchFor("foobar")
|
||||
expect(@view.$(".forum-nav-filter-main-control").val()).toEqual("all")
|
||||
|
||||
checkThreadsOrdering = (view, sort_order, type) ->
|
||||
expect(view.$el.find(".forum-nav-thread").children().length).toEqual(3)
|
||||
expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()).toEqual(sort_order[0])
|
||||
@@ -384,14 +415,21 @@ describe "DiscussionThreadListView", ->
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
expect(@view.addSearchAlert).not.toHaveBeenCalled()
|
||||
|
||||
describe "endorsed renders correctly", ->
|
||||
it "when absent", ->
|
||||
renderSingleThreadWithProps({})
|
||||
expect($(".forum-nav-thread-endorsed").length).toEqual(0)
|
||||
describe "post type renders correctly", ->
|
||||
it "for discussion", ->
|
||||
renderSingleThreadWithProps({thread_type: "discussion"})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("icon-comments")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion")
|
||||
|
||||
it "when present", ->
|
||||
renderSingleThreadWithProps({endorsed: true})
|
||||
expect($(".forum-nav-thread-endorsed").length).toEqual(1)
|
||||
it "for answered question", ->
|
||||
renderSingleThreadWithProps({thread_type: "question", endorsed: true})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("icon-ok")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("answered question")
|
||||
|
||||
it "for unanswered question", ->
|
||||
renderSingleThreadWithProps({thread_type: "question", endorsed: false})
|
||||
expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("icon-question")
|
||||
expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question")
|
||||
|
||||
describe "post labels render correctly", ->
|
||||
beforeEach ->
|
||||
@@ -487,7 +525,7 @@ describe "DiscussionThreadListView", ->
|
||||
expect(visibleItems).toEqual(expectedItems)
|
||||
|
||||
it "should be case-insensitive", ->
|
||||
checkFilter("flagged", ["Flagged Discussions"])
|
||||
checkFilter("other", ["Other Category"])
|
||||
|
||||
it "should match partial words", ->
|
||||
checkFilter("ateg", ["Other Category"])
|
||||
|
||||
@@ -1,89 +1,144 @@
|
||||
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"))
|
||||
@view.setElement($("#fixture-element"))
|
||||
@spyOn(@view, "convertMath")
|
||||
|
||||
it "renders the vote correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
|
||||
describe "voting", ->
|
||||
|
||||
it "votes correctly", ->
|
||||
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
|
||||
it "renders the vote state correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
|
||||
|
||||
it "unvotes correctly", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
|
||||
it "votes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("click"))
|
||||
|
||||
it 'toggles the vote correctly', ->
|
||||
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
|
||||
it "votes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
it "vote button activates on appropriate events", ->
|
||||
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
|
||||
it "unvotes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("click"))
|
||||
|
||||
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")
|
||||
it "unvotes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
# If pinning is not allowed, the pinning UI is not present, so no
|
||||
# test is needed
|
||||
describe "pinning", ->
|
||||
|
||||
describe "for a pinned thread", ->
|
||||
beforeEach ->
|
||||
@thread.set("pinned", true)
|
||||
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 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 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 "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 "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 "pinning button activates on appropriate events", ->
|
||||
DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".admin-pin")
|
||||
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*)$/)
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
describe "DiscussionThreadInlineView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<script type="text/template" id="_inline_thread">
|
||||
<article class="discussion-article">
|
||||
<div class="non-cohorted-indicator"/>
|
||||
<div class="post-body"/>
|
||||
<div class="post-extended-content">
|
||||
<div class="response-count"/>
|
||||
<ol class="responses"/>
|
||||
<div class="response-pagination"/>
|
||||
</div>
|
||||
<div class="post-tools">
|
||||
<a href="javascript:void(0)" class="expand-post">Expand</a>
|
||||
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
<script type="text/template" id="_inline_thread_cohorted">
|
||||
<article class="discussion-article">
|
||||
<div class="cohorted-indicator"/>
|
||||
<div class="post-body"/>
|
||||
<div class="post-extended-content">
|
||||
<div class="response-count"/>
|
||||
<ol class="responses"/>
|
||||
<div class="response-pagination"/>
|
||||
</div>
|
||||
<div class="post-tools">
|
||||
<a href="javascript:void(0)" class="expand-post">Expand</a>
|
||||
<a href="javascript:void(0)" class="collapse-post">Collapse</a>
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
<div class="thread-fixture"/>
|
||||
"""
|
||||
)
|
||||
|
||||
@threadData = {
|
||||
id: "dummy",
|
||||
body: "dummy body",
|
||||
abuse_flaggers: [],
|
||||
votes: {up_count: "42"}
|
||||
}
|
||||
@thread = new Thread(@threadData)
|
||||
@view = new DiscussionThreadInlineView({ model: @thread })
|
||||
@view.setElement($(".thread-fixture"))
|
||||
spyOn($, "ajax")
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(@view.showView, "render")
|
||||
spyOn(@view.showView, "convertMath")
|
||||
spyOn(@view, "makeWmdEditor")
|
||||
spyOn(DiscussionThreadView.prototype, "renderResponse")
|
||||
|
||||
assertContentVisible = (view, selector, visible) ->
|
||||
content = view.$el.find(selector)
|
||||
expect(content.length).toEqual(1)
|
||||
expect(content.is(":visible")).toEqual(visible)
|
||||
|
||||
assertExpandedContentVisible = (view, expanded) ->
|
||||
expect(view.$el.hasClass("expanded")).toEqual(expanded)
|
||||
assertContentVisible(view, ".post-extended-content", expanded)
|
||||
assertContentVisible(view, ".expand-post", not expanded)
|
||||
assertContentVisible(view, ".collapse-post", expanded)
|
||||
|
||||
describe "render", ->
|
||||
it "uses the cohorted template if cohorted", ->
|
||||
@view.model.set({group_id: 1})
|
||||
@view.render()
|
||||
expect(@view.$el.find(".cohorted-indicator").length).toEqual(1)
|
||||
|
||||
it "uses the non-cohorted template if not cohorted", ->
|
||||
@view.render()
|
||||
expect(@view.$el.find(".non-cohorted-indicator").length).toEqual(1)
|
||||
|
||||
it "shows content that should be visible when collapsed", ->
|
||||
@view.render()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "does not render any responses by default", ->
|
||||
@view.render()
|
||||
expect($.ajax).not.toHaveBeenCalled()
|
||||
expect(@view.$el.find(".responses li").length).toEqual(0)
|
||||
|
||||
describe "expand/collapse", ->
|
||||
it "shows/hides appropriate content", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@view.render()
|
||||
@view.expandPost()
|
||||
assertExpandedContentVisible(@view, true)
|
||||
@view.collapsePost()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "switches between the abbreviated and full body", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@thread.set("body", new Array(100).join("test "))
|
||||
@view.abbreviateBody()
|
||||
expect(@thread.get("body")).not.toEqual(@thread.get("abbreviatedBody"))
|
||||
@view.render()
|
||||
@view.expandPost()
|
||||
expect(@view.$el.find(".post-body").text()).toEqual(@thread.get("body"))
|
||||
expect(@view.showView.convertMath).toHaveBeenCalled()
|
||||
@view.showView.convertMath.reset()
|
||||
@view.collapsePost()
|
||||
expect(@view.$el.find(".post-body").text()).toEqual(@thread.get("abbreviatedBody"))
|
||||
expect(@view.showView.convertMath).toHaveBeenCalled()
|
||||
@@ -1,80 +1,188 @@
|
||||
describe "DiscussionThreadView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<script type="text/template" id="thread-template">
|
||||
<article class="discussion-article">
|
||||
<div class="response-count"/>
|
||||
<ol class="responses"/>
|
||||
<div class="response-pagination"/>
|
||||
</article>
|
||||
</script>
|
||||
<div class="thread-fixture"/>
|
||||
"""
|
||||
)
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
jasmine.Clock.useMock()
|
||||
@threadData = {
|
||||
id: "dummy"
|
||||
}
|
||||
@threadData = DiscussionViewSpecHelper.makeThreadWithProps({})
|
||||
@thread = new Thread(@threadData)
|
||||
@view = new DiscussionThreadView({ model: @thread })
|
||||
@view.setElement($(".thread-fixture"))
|
||||
spyOn($, "ajax")
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(@view.showView, "render")
|
||||
spyOn(@view, "makeWmdEditor")
|
||||
spyOn(DiscussionThreadView.prototype, "renderResponse")
|
||||
spyOn(DiscussionThreadShowView.prototype, "convertMath")
|
||||
spyOn(DiscussionContentView.prototype, "makeWmdEditor")
|
||||
spyOn(ThreadResponseView.prototype, "renderShowView")
|
||||
|
||||
describe "response count and pagination", ->
|
||||
renderWithContent = (view, content) ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent(content)
|
||||
view.render()
|
||||
jasmine.Clock.tick(100)
|
||||
renderWithContent = (view, content) ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent(content)
|
||||
view.render()
|
||||
jasmine.Clock.tick(100)
|
||||
|
||||
assertRenderedCorrectly = (view, countText, displayCountText, buttonText) ->
|
||||
expect(view.$el.find(".response-count").text()).toEqual(countText)
|
||||
if displayCountText
|
||||
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText)
|
||||
else
|
||||
expect(view.$el.find(".response-display-count").length).toEqual(0)
|
||||
if buttonText
|
||||
expect(view.$el.find(".load-response-button").text()).toEqual(buttonText)
|
||||
else
|
||||
expect(view.$el.find(".load-response-button").length).toEqual(0)
|
||||
assertContentVisible = (view, selector, visible) ->
|
||||
content = view.$el.find(selector)
|
||||
expect(content.length).toBeGreaterThan(0)
|
||||
content.each (i, elem) ->
|
||||
expect($(elem).is(":visible")).toEqual(visible)
|
||||
|
||||
it "correctly render for a thread with no responses", ->
|
||||
renderWithContent(@view, {resp_total: 0, children: []})
|
||||
assertRenderedCorrectly(@view, "0 responses", null, null)
|
||||
assertExpandedContentVisible = (view, expanded) ->
|
||||
expect(view.$el.hasClass("expanded")).toEqual(expanded)
|
||||
assertContentVisible(view, ".post-extended-content", expanded)
|
||||
assertContentVisible(view, ".forum-thread-expand", not expanded)
|
||||
assertContentVisible(view, ".forum-thread-collapse", expanded)
|
||||
|
||||
it "correctly render for a thread with one response", ->
|
||||
renderWithContent(@view, {resp_total: 1, children: [{}]})
|
||||
assertRenderedCorrectly(@view, "1 response", "Showing all responses", null)
|
||||
assertResponseCountAndPaginationCorrect = (view, countText, displayCountText, buttonText) ->
|
||||
expect(view.$el.find(".response-count").text()).toEqual(countText)
|
||||
if displayCountText
|
||||
expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText)
|
||||
else
|
||||
expect(view.$el.find(".response-display-count").length).toEqual(0)
|
||||
if buttonText
|
||||
expect(view.$el.find(".load-response-button").text()).toEqual(buttonText)
|
||||
else
|
||||
expect(view.$el.find(".load-response-button").length).toEqual(0)
|
||||
|
||||
it "correctly render for a thread with one additional page", ->
|
||||
renderWithContent(@view, {resp_total: 2, children: [{}]})
|
||||
assertRenderedCorrectly(@view, "2 responses", "Showing first response", "Load all responses")
|
||||
describe "tab mode", ->
|
||||
beforeEach ->
|
||||
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "tab"})
|
||||
|
||||
it "correctly render for a thread with multiple additional pages", ->
|
||||
renderWithContent(@view, {resp_total: 111, children: [{}, {}]})
|
||||
assertRenderedCorrectly(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses")
|
||||
describe "response count and pagination", ->
|
||||
it "correctly render for a thread with no responses", ->
|
||||
renderWithContent(@view, {resp_total: 0, children: []})
|
||||
assertResponseCountAndPaginationCorrect(@view, "0 responses", null, null)
|
||||
|
||||
describe "on clicking the load more button", ->
|
||||
beforeEach ->
|
||||
renderWithContent(@view, {resp_total: 5, children: [{}]})
|
||||
assertRenderedCorrectly(@view, "5 responses", "Showing first response", "Load all responses")
|
||||
|
||||
it "correctly re-render when all threads have loaded", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 5, children: [{}, {}, {}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertRenderedCorrectly(@view, "5 responses", "Showing all responses", null)
|
||||
it "correctly render for a thread with one response", ->
|
||||
renderWithContent(@view, {resp_total: 1, children: [{}]})
|
||||
assertResponseCountAndPaginationCorrect(@view, "1 response", "Showing all responses", null)
|
||||
|
||||
it "correctly re-render when one page remains", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 42, children: [{}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertRenderedCorrectly(@view, "42 responses", "Showing first 3 responses", "Load all responses")
|
||||
it "correctly render for a thread with one additional page", ->
|
||||
renderWithContent(@view, {resp_total: 2, children: [{}]})
|
||||
assertResponseCountAndPaginationCorrect(@view, "2 responses", "Showing first response", "Load all responses")
|
||||
|
||||
it "correctly re-render when multiple pages remain", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 111, children: [{}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertRenderedCorrectly(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses")
|
||||
it "correctly render for a thread with multiple additional pages", ->
|
||||
renderWithContent(@view, {resp_total: 111, children: [{}, {}]})
|
||||
assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses")
|
||||
|
||||
describe "on clicking the load more button", ->
|
||||
beforeEach ->
|
||||
renderWithContent(@view, {resp_total: 5, children: [{}]})
|
||||
assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing first response", "Load all responses")
|
||||
|
||||
it "correctly re-render when all threads have loaded", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 5, children: [{}, {}, {}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing all responses", null)
|
||||
|
||||
it "correctly re-render when one page remains", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 42, children: [{}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "42 responses", "Showing first 3 responses", "Load all responses")
|
||||
|
||||
it "correctly re-render when multiple pages remain", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 111, children: [{}, {}]})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses")
|
||||
|
||||
describe "inline mode", ->
|
||||
beforeEach ->
|
||||
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "inline"})
|
||||
|
||||
describe "render", ->
|
||||
it "shows content that should be visible when collapsed", ->
|
||||
@view.render()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "does not render any responses by default", ->
|
||||
@view.render()
|
||||
expect($.ajax).not.toHaveBeenCalled()
|
||||
expect(@view.$el.find(".responses li").length).toEqual(0)
|
||||
|
||||
describe "expand/collapse", ->
|
||||
it "shows/hides appropriate content", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
@view.render()
|
||||
@view.expand()
|
||||
assertExpandedContentVisible(@view, true)
|
||||
@view.collapse()
|
||||
assertExpandedContentVisible(@view, false)
|
||||
|
||||
it "switches between the abbreviated and full body", ->
|
||||
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
|
||||
longBody = new Array(100).join("test ")
|
||||
expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140)
|
||||
@thread.set("body", longBody)
|
||||
|
||||
@view.render()
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
DiscussionThreadShowView.prototype.convertMath.reset()
|
||||
|
||||
@view.expand()
|
||||
expect($(".post-body").text()).toEqual(longBody)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
DiscussionThreadShowView.prototype.convertMath.reset()
|
||||
|
||||
@view.collapse()
|
||||
expect($(".post-body").text()).toEqual(expectedAbbreviation)
|
||||
expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled()
|
||||
|
||||
describe "for question threads", ->
|
||||
beforeEach ->
|
||||
@thread.set("thread_type", "question")
|
||||
@view = new DiscussionThreadView(
|
||||
{model: @thread, el: $("#fixture-element"), mode: "tab"}
|
||||
)
|
||||
|
||||
renderTestCase = (view, numEndorsed, numNonEndorsed) ->
|
||||
generateContent = (idStart, idEnd) ->
|
||||
_.map(_.range(idStart, idEnd), (i) -> {"id": "#{i}"})
|
||||
renderWithContent(
|
||||
view,
|
||||
{
|
||||
endorsed_responses: generateContent(0, numEndorsed),
|
||||
non_endorsed_responses: generateContent(numEndorsed, numEndorsed + numNonEndorsed),
|
||||
non_endorsed_resp_total: 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(
|
||||
"#{numNonEndorsed} #{if numEndorsed then "other " else ""}response",
|
||||
"#{numNonEndorsed} #{if numEndorsed then "other " else ""}responses",
|
||||
numNonEndorsed
|
||||
)
|
||||
if numNonEndorsed then "Showing all responses" else null,
|
||||
null
|
||||
)
|
||||
|
||||
_.each({"no": 0, "one": 1, "many": 5}, (numEndorsed, endorsedDesc) ->
|
||||
_.each({"no": 0, "one": 1, "many": 5}, (numNonEndorsed, nonEndorsedDesc) ->
|
||||
it "renders correctly with #{endorsedDesc} marked answer(s) and #{nonEndorsedDesc} response(s)", ->
|
||||
renderTestCase(@view, numEndorsed, numNonEndorsed)
|
||||
)
|
||||
)
|
||||
|
||||
it "handles pagination correctly", ->
|
||||
renderWithContent(
|
||||
@view,
|
||||
{
|
||||
endorsed_responses: [{id: "1"}, {id: "2"}],
|
||||
non_endorsed_responses: [{id: "3"}, {id: "4"}, {id: "5"}],
|
||||
non_endorsed_resp_total: 42
|
||||
}
|
||||
)
|
||||
DiscussionViewSpecHelper.setNextResponseContent({
|
||||
# Add an endorsed response; it should be rendered
|
||||
endorsed_responses: [{id: "1"}, {id: "2"}, {id: "6"}],
|
||||
non_endorsed_responses: [{id: "7"}, {id: "8"}, {id: "9"}],
|
||||
non_endorsed_resp_total: 41
|
||||
})
|
||||
@view.$el.find(".load-response-button").click()
|
||||
expect($(".js-marked-answer-list .discussion-response").length).toEqual(3)
|
||||
expect($(".js-response-list .discussion-response").length).toEqual(6)
|
||||
assertResponseCountAndPaginationCorrect(
|
||||
@view,
|
||||
"41 other responses",
|
||||
"Showing first 6 responses",
|
||||
"Load all responses"
|
||||
)
|
||||
|
||||
@@ -1,101 +1,63 @@
|
||||
class @DiscussionViewSpecHelper
|
||||
@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)")
|
||||
@makeThreadWithProps = (props) ->
|
||||
# Minimal set of properties necessary for rendering
|
||||
thread = {
|
||||
id: "dummy_id",
|
||||
thread_type: "discussion",
|
||||
pinned: false,
|
||||
endorsed: false,
|
||||
votes: {up_count: '0'},
|
||||
unread_comments_count: 0,
|
||||
comments_count: 0,
|
||||
abuse_flaggers: [],
|
||||
body: "",
|
||||
title: "dummy title",
|
||||
created_at: "2014-08-18T01:02:03Z"
|
||||
}
|
||||
$.extend(thread, props)
|
||||
|
||||
@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)
|
||||
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: "43"}})
|
||||
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.vote()
|
||||
expect(window.user.voted(model)).toBe(true)
|
||||
if checkRendering
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, true)
|
||||
view.render()
|
||||
view.$el.find(".action-vote").trigger(event)
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
$.ajax.reset()
|
||||
deferred.resolve()
|
||||
|
||||
# Check idempotence
|
||||
view.vote()
|
||||
expect(window.user.voted(model)).toBe(true)
|
||||
if checkRendering
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, true)
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
@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, modelData, checkRendering) ->
|
||||
window.user.vote(model)
|
||||
expect(window.user.voted(model)).toBe(true)
|
||||
if checkRendering
|
||||
DiscussionViewSpecHelper.expectVoteRendered(view, true)
|
||||
|
||||
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: ->}
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
@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)
|
||||
@@ -111,7 +73,7 @@ class @DiscussionViewSpecHelper
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
@checkVoteButtonEvents = (view) ->
|
||||
@checkButtonEvents(view, "toggleVote", ".vote-btn")
|
||||
@checkButtonEvents(view, "toggleVote", ".action-vote")
|
||||
|
||||
@setNextResponseContent = (content) ->
|
||||
$.ajax.andCallFake(
|
||||
|
||||
@@ -3,101 +3,53 @@ describe "NewPostView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<article class="new-post-article" style="display: block;">
|
||||
<div class="inner-wrapper">
|
||||
<form class="new-post-form">
|
||||
<div class="left-column" >
|
||||
</div>
|
||||
</form>
|
||||
<div class="discussion-body">
|
||||
<div class="discussion-column">
|
||||
<article class="new-post-article" style="display: block;"></article>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-tab-template">
|
||||
<div class="inner-wrapper">
|
||||
<form class="new-post-form">
|
||||
<div class="left-column">
|
||||
'<%= topic_dropdown_html %>
|
||||
'<%= options_html %>'
|
||||
<script aria-hidden="true" type="text/template" id="new-post-template">
|
||||
<form class="forum-new-post-form">
|
||||
<% 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">
|
||||
<ul class="topic-menu" role="menu"><%= topics_html %></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-inline-template">
|
||||
<div class="inner-wrapper">
|
||||
<div class="new-post-form-errors">
|
||||
</div>
|
||||
<form class="new-post-form">
|
||||
<%= editor_html %>
|
||||
<%= options_html %>
|
||||
</form>
|
||||
</div>
|
||||
<% } %>
|
||||
<select class="js-group-select">
|
||||
<option value="">All Groups</option>
|
||||
<option value="1">Group 1</option>
|
||||
<option value="2">Group 2</option>
|
||||
</select>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-menu-entry-template">
|
||||
<li role="menuitem"><a href="#" class="topic" data-discussion_id="<%- id %>" aria-describedby="topic-name-span-<%- id %>" cohorted="<%- is_cohorted %>"><%- text %></a></li>
|
||||
<li role="menuitem">
|
||||
<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">
|
||||
<a href="#"><span class="category-menu-span"><%- text %></span></a>
|
||||
<ul role="menu"><%= entries %></ul>
|
||||
<span class="topic-title"><%- text %></span>
|
||||
<ul role="menu" class="topic-submenu"><%= entries %></ul>
|
||||
</li>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-topic-dropdown-template">
|
||||
<span class="topic-dropdown-label" id="topic-dropdown-label">Create new post about:</span>
|
||||
<div class="form-topic-drop">
|
||||
<a href="#" aria-labelledby="topic-dropdown-label" class="topic_dropdown_button">${_("Show All Discussions")}<span class="drop-arrow" aria-hidden="true">▾</span></a>
|
||||
<div class="topic_menu_wrapper">
|
||||
<div class="topic_menu_search" role="menu">
|
||||
<label class="sr" for="browse-topic-newpost">Filter List</label>
|
||||
<input type="text" id="browse-topic-newpost" class="form-topic-drop-search-input" placeholder="Filter discussion areas">
|
||||
</div>
|
||||
<ul class="topic_menu" role="menu"><%= topics_html %></ul>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-options-template">
|
||||
<div class="options">
|
||||
<input type="checkbox" name="follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
|
||||
<% if (allow_anonymous) { %>
|
||||
<br>
|
||||
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous">
|
||||
<label for="new-post-anonymous">post anonymously</label>
|
||||
<% } %>
|
||||
<% if (allow_anonymous_to_peers) { %>
|
||||
<br>
|
||||
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers">
|
||||
<label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
|
||||
<% } %>
|
||||
<% if (cohort_options) { %>
|
||||
<div class="form-group-label choose-cohort">
|
||||
## Translators: This labels the selector for which group of students can view a post
|
||||
Make visible to:
|
||||
<select class="group-filter-select new-post-group" 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>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="new-post-editor-template">
|
||||
<div class="form-row">
|
||||
<label class="sr" for="new-post-title">new post title</label>
|
||||
<input type="text" id="new-post-title" class="new-post-title" name="title" placeholder="Title">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="new-post-body" name="body" placeholder="Enter your question or comment…"></div>
|
||||
</div>
|
||||
<input type="submit" id="new-post-submit" class="submit" value="Add post">
|
||||
<a href="#" class="new-post-cancel">Cancel</a>
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
window.$$course_id = "edX/999/test"
|
||||
@@ -106,30 +58,31 @@ describe "NewPostView", ->
|
||||
|
||||
describe "Drop down works correct", ->
|
||||
beforeEach ->
|
||||
@course_settings = new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"subcategories": {
|
||||
"Basic Question Types": {
|
||||
"subcategories": {},
|
||||
"children": ["Selection From Options"],
|
||||
"entries": {
|
||||
"Selection From Options": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": true,
|
||||
"id": "cba3e4cd91d0466b9ac50926e495b76f"
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"children": ["Basic Question Types"],
|
||||
"entries": {}
|
||||
},
|
||||
"allow_anonymous": true,
|
||||
"allow_anonymous_to_peers": true
|
||||
})
|
||||
@view = new NewPostView(
|
||||
el: $(".new-post-article"),
|
||||
collection: @discussion,
|
||||
course_settings: new DiscussionCourseSettings({
|
||||
"category_map": {
|
||||
"subcategories": {
|
||||
"Basic Question Types": {
|
||||
"subcategories": {},
|
||||
"children": ["Selection From Options"],
|
||||
"entries": {
|
||||
"Selection From Options": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": true,
|
||||
"id": "cba3e4cd91d0466b9ac50926e495b76f"
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"children": ["Basic Question Types"],
|
||||
"entries": {}
|
||||
},
|
||||
"allow_anonymous": true,
|
||||
"allow_anonymous_to_peers": true
|
||||
}),
|
||||
course_settings: @course_settings,
|
||||
mode: "tab"
|
||||
)
|
||||
@view.render()
|
||||
@@ -140,16 +93,16 @@ describe "NewPostView", ->
|
||||
complete_text = @parent_category_text + " / " + @selected_option_text
|
||||
selected_text_width = @view.getNameWidth(complete_text)
|
||||
@view.maxNameWidth = selected_text_width + 1
|
||||
@view.$el.find( "ul.topic_menu li[role='menuitem'] > a" )[1].click()
|
||||
dropdown_text = @view.$el.find(".form-topic-drop > a").text()
|
||||
expect(complete_text+' ▾').toEqual(dropdown_text)
|
||||
@view.$el.find( "a.topic-title" ).first().click()
|
||||
dropdown_text = @view.$el.find(".js-selected-topic").text()
|
||||
expect(complete_text).toEqual(dropdown_text)
|
||||
|
||||
it "completely show just sub-category", ->
|
||||
complete_text = @parent_category_text + " / " + @selected_option_text
|
||||
selected_text_width = @view.getNameWidth(complete_text)
|
||||
@view.maxNameWidth = selected_text_width - 10
|
||||
@view.$el.find( "ul.topic_menu li[role='menuitem'] > a" )[1].click()
|
||||
dropdown_text = @view.$el.find(".form-topic-drop > a").text()
|
||||
@view.$el.find( "a.topic-title" ).first().click()
|
||||
dropdown_text = @view.$el.find(".js-selected-topic").text()
|
||||
expect(dropdown_text.indexOf("…")).toEqual(0)
|
||||
expect(dropdown_text).toContain(@selected_option_text)
|
||||
|
||||
@@ -158,8 +111,8 @@ describe "NewPostView", ->
|
||||
complete_text = @parent_category_text + " / " + @selected_option_text
|
||||
selected_text_width = @view.getNameWidth(complete_text)
|
||||
@view.maxNameWidth = selected_text_width - parent_width
|
||||
@view.$el.find( "ul.topic_menu li[role='menuitem'] > a" )[1].click()
|
||||
dropdown_text = @view.$el.find(".form-topic-drop > a").text()
|
||||
@view.$el.find( "a.topic-title" ).first().click()
|
||||
dropdown_text = @view.$el.find(".js-selected-topic").text()
|
||||
expect(dropdown_text.indexOf("…")).toEqual(0)
|
||||
expect(dropdown_text.lastIndexOf("…")).toBeGreaterThan(0)
|
||||
|
||||
@@ -167,10 +120,49 @@ describe "NewPostView", ->
|
||||
complete_text = @parent_category_text + " / " + @selected_option_text
|
||||
selected_text_width = @view.getNameWidth(complete_text)
|
||||
@view.maxNameWidth = @view.getNameWidth(@selected_option_text) + 100
|
||||
@view.$el.find( "ul.topic_menu li[role='menuitem'] > a" )[1].click()
|
||||
dropdown_text = @view.$el.find(".form-topic-drop > a").text()
|
||||
@view.$el.find( "a.topic-title" ).first().click()
|
||||
dropdown_text = @view.$el.find(".js-selected-topic").text()
|
||||
expect(dropdown_text.indexOf("/ span>")).toEqual(-1)
|
||||
|
||||
describe "cohort selector", ->
|
||||
renderWithCohortedTopics = (course_settings, view, isCohortedFirst) ->
|
||||
course_settings.set(
|
||||
"category_map",
|
||||
{
|
||||
"children": if isCohortedFirst then ["Cohorted", "Non-Cohorted"] else ["Non-Cohorted", "Cohorted"],
|
||||
"entries": {
|
||||
"Non-Cohorted": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": false,
|
||||
"id": "non-cohorted"
|
||||
},
|
||||
"Cohorted": {
|
||||
"sort_key": null,
|
||||
"is_cohorted": true,
|
||||
"id": "cohorted"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
view.render()
|
||||
|
||||
expectCohortSelectorEnabled = (view, enabled) ->
|
||||
expect(view.$(".js-group-select").prop("disabled")).toEqual(not enabled)
|
||||
if not enabled
|
||||
expect(view.$(".js-group-select option:selected").attr("value")).toEqual("")
|
||||
|
||||
it "is disabled with non-cohorted default topic and enabled by selecting cohorted topic", ->
|
||||
renderWithCohortedTopics(@course_settings, @view, false)
|
||||
expectCohortSelectorEnabled(@view, false)
|
||||
@view.$("a.topic-title[data-discussion-id=cohorted]").click()
|
||||
expectCohortSelectorEnabled(@view, true)
|
||||
|
||||
it "is enabled with cohorted default topic and disabled by selecting non-cohorted topic", ->
|
||||
renderWithCohortedTopics(@course_settings, @view, true)
|
||||
expectCohortSelectorEnabled(@view, true)
|
||||
@view.$("a.topic-title[data-discussion-id=non-cohorted]").click()
|
||||
expectCohortSelectorEnabled(@view, false)
|
||||
|
||||
it "posts to the correct URL", ->
|
||||
topicId = "test_topic"
|
||||
spyOn($, "ajax").andCallFake(
|
||||
@@ -189,5 +181,5 @@ describe "NewPostView", ->
|
||||
topicId: topicId
|
||||
)
|
||||
view.render()
|
||||
view.$(".new-post-form").submit()
|
||||
view.$(".forum-new-post-form").submit()
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
|
||||
@@ -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">–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(
|
||||
|
||||
@@ -1,40 +1,220 @@
|
||||
describe "ThreadResponseShowView", ->
|
||||
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>
|
||||
"""
|
||||
)
|
||||
DiscussionSpecHelper.setUnderscoreFixtures()
|
||||
|
||||
@user = DiscussionUtil.getUser()
|
||||
@thread = new Thread({"thread_type": "discussion"})
|
||||
@commentData = {
|
||||
id: "dummy",
|
||||
user_id: "567",
|
||||
course_id: "TestOrg/TestCourse/TestRun",
|
||||
body: "this is a comment",
|
||||
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)
|
||||
@view = new ThreadResponseShowView({ model: @comment })
|
||||
@view.setElement($(".discussion-post"))
|
||||
@comment.set("thread", @thread)
|
||||
@view = new ThreadResponseShowView({ model: @comment, $el: $("#fixture-element") })
|
||||
|
||||
it "renders the vote correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
|
||||
# Avoid unnecessary boilerplate
|
||||
spyOn(ThreadResponseShowView.prototype, "convertMath")
|
||||
|
||||
it "votes correctly", ->
|
||||
DiscussionViewSpecHelper.checkVote(@view, @comment, @commentData, true)
|
||||
@view.render()
|
||||
|
||||
it "unvotes correctly", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @commentData, true)
|
||||
describe "voting", ->
|
||||
|
||||
it 'toggles the vote correctly', ->
|
||||
DiscussionViewSpecHelper.checkToggleVote(@view, @comment)
|
||||
it "renders the vote state correctly", ->
|
||||
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
|
||||
|
||||
it "vote button activates on appropriate events", ->
|
||||
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)
|
||||
it "votes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("click"))
|
||||
|
||||
it "votes correctly via spacebar", ->
|
||||
DiscussionViewSpecHelper.checkUpvote(@view, @comment, @user, $.Event("keydown", {which: 32}))
|
||||
|
||||
it "unvotes correctly via click", ->
|
||||
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @user, $.Event("click"))
|
||||
|
||||
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 = {
|
||||
"username": "test_endorser",
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "question")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text().replace(/\s+/g, " ")).toMatch(
|
||||
"marked as answer less than a minute ago by " + endorsement.username
|
||||
)
|
||||
|
||||
it "renders anonymous endorsement correctly for a marked answer in a question thread", ->
|
||||
endorsement = {
|
||||
"username": null,
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@thread.set("thread_type", "question")
|
||||
@comment.set({
|
||||
"endorsed": true,
|
||||
"endorsement": endorsement
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".posted-details").text()).toMatch("marked as answer less than a minute ago")
|
||||
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-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", ->
|
||||
DiscussionUtil.loadRoles({"Moderator": parseInt(window.user.id)})
|
||||
@thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
|
||||
endorseButton.click()
|
||||
expect(endorseButton).toHaveClass("is-checked")
|
||||
|
||||
it "allows the author of a question thread to mark an answer", ->
|
||||
@thread.set({
|
||||
"thread_type": "question",
|
||||
"user_id": window.user.id
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
expect(endorseButton.closest(".actions-item")).not.toHaveClass("is-hidden")
|
||||
endorseButton.click()
|
||||
expect(endorseButton).toHaveClass("is-checked")
|
||||
|
||||
it "does not allow the author of a discussion thread to endorse", ->
|
||||
@thread.set({
|
||||
"thread_type": "discussion",
|
||||
"user_id": window.user.id
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-endorse")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
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({
|
||||
"thread_type": "question",
|
||||
"user_id": (parseInt(window.user.id) + 1).toString()
|
||||
})
|
||||
@view.render()
|
||||
endorseButton = @view.$(".action-answer")
|
||||
expect(endorseButton.length).toEqual(1)
|
||||
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)
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
describe 'ThreadResponseView', ->
|
||||
beforeEach ->
|
||||
setFixtures """
|
||||
<script id="thread-response-template" type="text/template">
|
||||
<div/>
|
||||
</script>
|
||||
<div id="thread-response-fixture"/>
|
||||
"""
|
||||
DiscussionSpecHelper.setUpGlobals()
|
||||
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")
|
||||
|
||||
describe 'renderComments', ->
|
||||
it 'hides "show comments" link if collapseComments is not set', ->
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'hides "show comments" link if collapseComments is set but response has no comments', ->
|
||||
@response = new Comment { children: [] }
|
||||
@view = new ThreadResponseView({
|
||||
model: @response, el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'hides comments if collapseComments is set and shows them when "show comments" link is clicked', ->
|
||||
@view = new ThreadResponseView({
|
||||
model: @response, el: $("#fixture-element"),
|
||||
collapseComments: true
|
||||
})
|
||||
@view.render()
|
||||
expect(@view.$(".comments")).not.toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).toBeVisible()
|
||||
@view.$(".action-show-comments").click()
|
||||
expect(@view.$(".comments")).toBeVisible()
|
||||
expect(@view.$(".action-show-comments")).not.toBeVisible()
|
||||
|
||||
it 'populates commentViews and binds events', ->
|
||||
# Ensure that edit view is set to test invocation of cancelEdit
|
||||
@view.createEditView()
|
||||
|
||||
@@ -9,7 +9,6 @@ if Backbone?
|
||||
actions:
|
||||
editable: '.admin-edit'
|
||||
can_reply: '.discussion-reply'
|
||||
can_endorse: '.admin-endorse'
|
||||
can_delete: '.admin-delete'
|
||||
can_openclose: '.admin-openclose'
|
||||
|
||||
@@ -21,6 +20,9 @@ if Backbone?
|
||||
can: (action) ->
|
||||
(@get('ability') || {})[action]
|
||||
|
||||
# Default implementation
|
||||
canBeEndorsed: -> false
|
||||
|
||||
updateInfo: (info) ->
|
||||
if info
|
||||
@set('ability', info.ability)
|
||||
@@ -106,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:
|
||||
@@ -187,6 +197,13 @@ if Backbone?
|
||||
count += comment.getCommentsCount() + 1
|
||||
count
|
||||
|
||||
canBeEndorsed: =>
|
||||
user_id = window.user.get("id")
|
||||
user_id && (
|
||||
DiscussionUtil.isPrivilegedUser(user_id) ||
|
||||
(@get('thread').get('thread_type') == 'question' && @get('thread').get('user_id') == user_id)
|
||||
)
|
||||
|
||||
class @Comments extends Backbone.Collection
|
||||
|
||||
model: Comment
|
||||
|
||||
@@ -34,6 +34,8 @@ if Backbone?
|
||||
|
||||
retrieveAnotherPage: (mode, options={}, sort_options={}, error=null)->
|
||||
data = { page: @current_page + 1 }
|
||||
if _.contains(["unread", "unanswered", "flagged"], options.filter)
|
||||
data[options.filter] = true
|
||||
switch mode
|
||||
when 'search'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
@@ -43,9 +45,6 @@ if Backbone?
|
||||
data['commentable_ids'] = options.commentable_ids
|
||||
when 'all'
|
||||
url = DiscussionUtil.urlFor 'threads'
|
||||
when 'flagged'
|
||||
data['flagged'] = true
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
when 'followed'
|
||||
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
|
||||
if options['group_id']
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
class @DiscussionFilter
|
||||
|
||||
# TODO: this helper class duplicates functionality in DiscussionThreadListView.filterTopics
|
||||
# for use with a very similar category dropdown in the New Post form. The two menus' implementations
|
||||
# should be merged into a single reusable view.
|
||||
|
||||
@filterDrop: (e) ->
|
||||
$drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper')
|
||||
$drop = $(e.target).parents('.topic-menu-wrapper')
|
||||
query = $(e.target).val()
|
||||
$items = $drop.find('a')
|
||||
$items = $drop.find('.topic-menu-item')
|
||||
|
||||
if(query.length == 0)
|
||||
$items.removeClass('hidden')
|
||||
@@ -10,19 +15,14 @@ class @DiscussionFilter
|
||||
|
||||
$items.addClass('hidden')
|
||||
$items.each (i) ->
|
||||
thisText = $(this).not('.unread').text()
|
||||
$(this).parents('ul').siblings('a').not('.unread').each (i) ->
|
||||
thisText = thisText + ' ' + $(this).text();
|
||||
|
||||
test = true
|
||||
terms = thisText.split(' ')
|
||||
path = $(this).parents(".topic-menu-item").andSelf()
|
||||
pathTitles = path.children(".topic-title").map((i, elem) -> $(elem).text()).get()
|
||||
pathText = pathTitles.join(" / ").toLowerCase()
|
||||
|
||||
if(thisText.toLowerCase().search(query.toLowerCase()) == -1)
|
||||
test = false
|
||||
|
||||
if(test)
|
||||
if query.split(" ").every((term) -> pathText.search(term.toLowerCase()) != -1)
|
||||
$(this).removeClass('hidden')
|
||||
# show children
|
||||
$(this).parent().find('a').removeClass('hidden');
|
||||
$(this).find('.topic-menu-item').removeClass('hidden');
|
||||
# show parents
|
||||
$(this).parents('ul').siblings('a').removeClass('hidden');
|
||||
$(this).parents('.topic-menu-item').removeClass('hidden');
|
||||
|
||||
@@ -7,7 +7,7 @@ if Backbone?
|
||||
"click .new-post-btn": "toggleNewPost"
|
||||
"keydown .new-post-btn":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleNewPost)
|
||||
"click .new-post-cancel": "hideNewPost"
|
||||
"click .cancel": "hideNewPost"
|
||||
"click .discussion-paginator a": "navigateToPage"
|
||||
|
||||
paginationTemplate: -> DiscussionUtil.getTemplate("_pagination")
|
||||
@@ -101,7 +101,7 @@ if Backbone?
|
||||
|
||||
@newPostForm = $('.new-post-article')
|
||||
@threadviews = @discussion.map (thread) ->
|
||||
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
|
||||
new DiscussionThreadView el: @$("article#thread_#{thread.id}"), model: thread, mode: "inline"
|
||||
_.each @threadviews, (dtv) -> dtv.render()
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
@newPostView = new NewPostView(
|
||||
@@ -124,7 +124,7 @@ if Backbone?
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
|
||||
@$('section.discussion > .threads').prepend(article)
|
||||
threadView = new DiscussionThreadInlineView el: article, model: thread
|
||||
threadView = new DiscussionThreadView el: article, model: thread, mode: "inline"
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ if Backbone?
|
||||
@newPostView.render()
|
||||
$('.new-post-btn').bind "click", @showNewPost
|
||||
$('.new-post-btn').bind "keydown", (event) => DiscussionUtil.activateOnSpace(event, @showNewPost)
|
||||
$('.new-post-cancel').bind "click", @hideNewPost
|
||||
@newPostView.$('.cancel').bind "click", @hideNewPost
|
||||
|
||||
allThreads: ->
|
||||
@nav.updateSidebar()
|
||||
@@ -45,8 +45,12 @@ if Backbone?
|
||||
if(@main)
|
||||
@main.cleanup()
|
||||
@main.undelegateEvents()
|
||||
unless($(".forum-content").is(":visible"))
|
||||
$(".forum-content").fadeIn()
|
||||
if(@newPost.is(":visible"))
|
||||
@newPost.fadeOut()
|
||||
|
||||
@main = new DiscussionThreadView(el: $(".discussion-column"), model: @thread)
|
||||
@main = new DiscussionThreadView(el: $(".forum-content"), model: @thread, mode: "tab")
|
||||
@main.render()
|
||||
@main.on "thread:responses:rendered", =>
|
||||
@nav.updateSidebar()
|
||||
@@ -59,8 +63,17 @@ if Backbone?
|
||||
@navigate("", trigger: true)
|
||||
|
||||
showNewPost: (event) =>
|
||||
@newPost.slideDown(300)
|
||||
$('.new-post-title').focus()
|
||||
$('.forum-content').fadeOut(
|
||||
duration: 200
|
||||
complete: =>
|
||||
@newPost.fadeIn(200)
|
||||
$('.new-post-title').focus()
|
||||
)
|
||||
|
||||
hideNewPost: (event) =>
|
||||
@newPost.slideUp(300)
|
||||
@newPost.fadeOut(
|
||||
duration: 200
|
||||
complete: =>
|
||||
$('.forum-content').fadeIn(200)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -41,6 +40,9 @@ class @DiscussionUtil
|
||||
ta = _.union(@roleIds['Community TA'])
|
||||
_.include(ta, parseInt(user_id))
|
||||
|
||||
@isPrivilegedUser: (user_id) ->
|
||||
@isStaff(user_id) || @isTA(user_id)
|
||||
|
||||
@bulkUpdateContentInfo: (infos) ->
|
||||
for id, info of infos
|
||||
Content.getContent(id).updateInfo(info)
|
||||
@@ -159,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(' ')
|
||||
@@ -167,7 +176,7 @@ class @DiscussionUtil
|
||||
@formErrorHandler: (errorsField) ->
|
||||
(xhr, textStatus, error) ->
|
||||
makeErrorElem = (message) ->
|
||||
$("<li>").addClass("new-post-form-error").html(message)
|
||||
$("<li>").addClass("post-error").html(message)
|
||||
errorsField.empty().show()
|
||||
if xhr.status == 400
|
||||
response = JSON.parse(xhr.responseText)
|
||||
|
||||
@@ -8,40 +8,6 @@ if Backbone?
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
|
||||
|
||||
attrRenderer:
|
||||
endorsed: (endorsed) ->
|
||||
if endorsed
|
||||
@$(".action-endorse").show().addClass("is-endorsed")
|
||||
else
|
||||
if @model.get('ability')?.can_endorse
|
||||
@$(".action-endorse").show()
|
||||
else
|
||||
@$(".action-endorse").hide()
|
||||
@$(".action-endorse").removeClass("is-endorsed")
|
||||
|
||||
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]
|
||||
@@ -51,23 +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()
|
||||
can_endorse:
|
||||
enable: ->
|
||||
@$(".action-endorse").show().css("cursor", "auto")
|
||||
disable: ->
|
||||
@$(".action-endorse").css("cursor", "default")
|
||||
if not @model.get('endorsed')
|
||||
@$(".action-endorse").hide()
|
||||
else
|
||||
@$(".action-endorse").show()
|
||||
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()
|
||||
@@ -79,15 +44,6 @@ if Backbone?
|
||||
if @attrRenderer[attr]
|
||||
@attrRenderer[attr].apply(@, [value])
|
||||
|
||||
$: (selector) ->
|
||||
@$local.find(selector)
|
||||
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".local")
|
||||
if not @$local.length
|
||||
@$local = @$el
|
||||
@$delegateElement = @$local
|
||||
|
||||
makeWmdEditor: (cls_identifier) =>
|
||||
if not @$el.find(".wmd-panel").length
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), cls_identifier
|
||||
@@ -103,116 +59,238 @@ if Backbone?
|
||||
|
||||
|
||||
initialize: ->
|
||||
@initLocal()
|
||||
@model.bind('change', @renderPartialAttrs, @)
|
||||
|
||||
|
||||
toggleFollowing: (event) =>
|
||||
event.preventDefault()
|
||||
$elem = $(event.target)
|
||||
url = null
|
||||
if not @model.get('subscribed')
|
||||
@model.follow()
|
||||
url = @model.urlFor("follow")
|
||||
else
|
||||
@model.unfollow()
|
||||
url = @model.urlFor("unfollow")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
|
||||
toggleFlagAbuse: (event) =>
|
||||
event.preventDefault()
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@unFlagAbuse()
|
||||
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
|
||||
@listenTo(@model, "change:endorsed", =>
|
||||
if @model instanceof Comment
|
||||
@trigger("comment:endorse")
|
||||
)
|
||||
button.html("<span class='plus-icon'/>" + buttonText)
|
||||
|
||||
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()
|
||||
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
|
||||
$("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()
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
@vote()
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
null
|
||||
|
||||
@@ -10,6 +10,7 @@ if Backbone?
|
||||
"change .forum-nav-sort-control": "sortThreads"
|
||||
"click .forum-nav-thread-link": "threadSelected"
|
||||
"click .forum-nav-load-more-link": "loadMorePages"
|
||||
"change .forum-nav-filter-main-control": "chooseFilter"
|
||||
"change .forum-nav-filter-cohort-control": "chooseCohort"
|
||||
|
||||
initialize: ->
|
||||
@@ -75,7 +76,7 @@ if Backbone?
|
||||
#TODO fix this entire chain of events
|
||||
addAndSelectThread: (thread) =>
|
||||
commentable_id = thread.get("commentable_id")
|
||||
menuItem = @$(".forum-nav-browse-menu-item[data-discussion-id]").filter(-> $(this).data("discussion-id").id == commentable_id)
|
||||
menuItem = @$(".forum-nav-browse-menu-item[data-discussion-id]").filter(-> $(this).data("discussion-id") == commentable_id)
|
||||
@setCurrentTopicDisplay(@getPathText(menuItem))
|
||||
@retrieveDiscussion commentable_id, =>
|
||||
@trigger "thread:created", thread.get('id')
|
||||
@@ -173,7 +174,7 @@ if Backbone?
|
||||
loadingElem = loadMoreElem.find(".forum-nav-loading")
|
||||
DiscussionUtil.makeFocusTrap(loadingElem)
|
||||
loadingElem.focus()
|
||||
options = {}
|
||||
options = {filter: @filter}
|
||||
switch @mode
|
||||
when 'search'
|
||||
options.search_text = @current_search
|
||||
@@ -242,7 +243,7 @@ if Backbone?
|
||||
|
||||
goHome: ->
|
||||
@template = _.template($("#discussion-home").html())
|
||||
$(".discussion-column").html(@template)
|
||||
$(".forum-content").html(@template)
|
||||
$(".forum-nav-thread-list a").removeClass("is-active")
|
||||
$("input.email-setting").bind "click", @updateEmailNotifications
|
||||
url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
|
||||
@@ -363,26 +364,24 @@ if Backbone?
|
||||
@discussionIds = ""
|
||||
@$('.forum-nav-filter-cohort').show()
|
||||
@retrieveAllThreads()
|
||||
else if item.hasClass("forum-nav-browse-menu-flagged")
|
||||
@discussionIds = ""
|
||||
@$('.forum-nav-filter-cohort').hide()
|
||||
@retrieveFlaggedThreads()
|
||||
else if item.hasClass("forum-nav-browse-menu-following")
|
||||
@retrieveFollowed()
|
||||
@$('.forum-nav-filter-cohort').hide()
|
||||
else
|
||||
allItems = item.find(".forum-nav-browse-menu-item").andSelf()
|
||||
discussionIds = allItems.filter("[data-discussion-id]").map(
|
||||
(i, elem) -> $(elem).data("discussion-id").id
|
||||
(i, elem) -> $(elem).data("discussion-id")
|
||||
).get()
|
||||
@retrieveDiscussions(discussionIds)
|
||||
@$(".forum-nav-filter-cohort").toggle(item.data('cohorted') == true)
|
||||
|
||||
chooseCohort: (event) ->
|
||||
chooseFilter: (event) =>
|
||||
@filter = $(".forum-nav-filter-main-control :selected").val()
|
||||
@retrieveFirstPage()
|
||||
|
||||
chooseCohort: (event) =>
|
||||
@group_id = @$('.forum-nav-filter-cohort-control :selected').val()
|
||||
@collection.current_page = 0
|
||||
@collection.reset()
|
||||
@loadMorePages(event)
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveDiscussion: (discussion_id, callback=null) ->
|
||||
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
|
||||
@@ -413,12 +412,6 @@ if Backbone?
|
||||
@collection.reset()
|
||||
@loadMorePages(event)
|
||||
|
||||
retrieveFlaggedThreads: (event)->
|
||||
@collection.current_page = 0
|
||||
@collection.reset()
|
||||
@mode = 'flagged'
|
||||
@loadMorePages(event)
|
||||
|
||||
sortThreads: (event) ->
|
||||
@displayedCollection.setSortComparator(@$(".forum-nav-sort-control").val())
|
||||
|
||||
@@ -434,6 +427,7 @@ if Backbone?
|
||||
|
||||
searchFor: (text) ->
|
||||
@clearSearchAlerts()
|
||||
@clearFilters()
|
||||
@mode = 'search'
|
||||
@current_search = text
|
||||
url = DiscussionUtil.urlFor("search")
|
||||
@@ -499,6 +493,11 @@ if Backbone?
|
||||
clearSearch: ->
|
||||
@$(".forum-nav-search-input").val("")
|
||||
@current_search = ""
|
||||
@clearSearchAlerts()
|
||||
|
||||
clearFilters: ->
|
||||
@$(".forum-nav-filter-main-control").val("all")
|
||||
@$(".forum-nav-filter-cohort-control").val("all")
|
||||
|
||||
retrieveFollowed: () =>
|
||||
@mode = 'followed'
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
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)
|
||||
|
||||
initialize: ->
|
||||
class @DiscussionThreadShowView extends DiscussionContentShowView
|
||||
initialize: (options) ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
|
||||
if @mode not in ["tab", "inline"]
|
||||
throw new Error("invalid mode: " + @mode)
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-show-template").html())
|
||||
@template(@model.toJSON())
|
||||
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()
|
||||
@@ -44,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()
|
||||
@@ -109,74 +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(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"))
|
||||
|
||||
class @DiscussionThreadInlineShowView extends DiscussionThreadShowView
|
||||
renderTemplate: ->
|
||||
@template = DiscussionUtil.getTemplate('_inline_thread_show')
|
||||
params = @model.toJSON()
|
||||
if @model.get('username')?
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
Mustache.render(@template, params)
|
||||
|
||||
|
||||
|
||||
@@ -7,14 +7,25 @@ if Backbone?
|
||||
events:
|
||||
"click .discussion-submit-post": "submitComment"
|
||||
"click .add-response-btn": "scrollToAddResponse"
|
||||
"click .forum-thread-expand": "expand"
|
||||
"click .forum-thread-collapse": "collapse"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
isQuestion: ->
|
||||
@model.get("thread_type") == "question"
|
||||
|
||||
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)
|
||||
@createShowView()
|
||||
@responses = new Comments()
|
||||
@loadedResponses = false
|
||||
if @isQuestion()
|
||||
@markedAnswers = new Comments()
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-template").html())
|
||||
@@ -22,7 +33,6 @@ if Backbone?
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
|
||||
@renderShowView()
|
||||
@@ -31,11 +41,53 @@ if Backbone?
|
||||
@$("span.timeago").timeago()
|
||||
@makeWmdEditor "reply-body"
|
||||
@renderAddResponseButton()
|
||||
@responses.on("add", @renderResponse)
|
||||
# Without a delay, jQuery doesn't add the loading extension defined in
|
||||
# utils.coffee before safeAjax is invoked, which results in an error
|
||||
setTimeout((=> @loadInitialResponses()), 100)
|
||||
@
|
||||
@responses.on("add", (response) => @renderResponseToList(response, ".js-response-list", {}))
|
||||
if @isQuestion()
|
||||
@markedAnswers.on("add", (response) => @renderResponseToList(response, ".js-marked-answer-list", {collapseComments: true}))
|
||||
if @mode == "tab"
|
||||
# Without a delay, jQuery doesn't add the loading extension defined in
|
||||
# utils.coffee before safeAjax is invoked, which results in an error
|
||||
setTimeout((=> @loadInitialResponses()), 100)
|
||||
@$(".post-tools").hide()
|
||||
else # mode == "inline"
|
||||
@collapse()
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
closed: (closed) ->
|
||||
@$(".discussion-reply-new").toggle(not closed)
|
||||
@renderAddResponseButton()
|
||||
})
|
||||
|
||||
expand: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$el.addClass("expanded")
|
||||
@$el.find(".post-body").html(@model.get("body"))
|
||||
@showView.convertMath()
|
||||
@$el.find(".forum-thread-expand").hide()
|
||||
@$el.find(".forum-thread-collapse").show()
|
||||
@$el.find(".post-extended-content").show()
|
||||
if not @loadedResponses
|
||||
@loadInitialResponses()
|
||||
|
||||
collapse: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$el.removeClass("expanded")
|
||||
@$el.find(".post-body").html(@getAbbreviatedBody())
|
||||
@showView.convertMath()
|
||||
@$el.find(".forum-thread-expand").show()
|
||||
@$el.find(".forum-thread-collapse").hide()
|
||||
@$el.find(".post-extended-content").hide()
|
||||
|
||||
getAbbreviatedBody: ->
|
||||
cached = @model.get("abbreviatedBody")
|
||||
if cached
|
||||
cached
|
||||
else
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get("body"), 140
|
||||
@model.set("abbreviatedBody", abbreviated)
|
||||
abbreviated
|
||||
|
||||
cleanup: ->
|
||||
if @responsesRequest?
|
||||
@@ -54,9 +106,20 @@ if Backbone?
|
||||
@responseRequest = null
|
||||
success: (data, textStatus, xhr) =>
|
||||
Content.loadContentInfos(data['annotated_content_info'])
|
||||
@responses.add(data['content']['children'])
|
||||
@renderResponseCountAndPagination(data['content']['resp_total'])
|
||||
if @isQuestion()
|
||||
@markedAnswers.add(data["content"]["endorsed_responses"])
|
||||
@responses.add(
|
||||
if @isQuestion()
|
||||
then data["content"]["non_endorsed_responses"]
|
||||
else data["content"]["children"]
|
||||
)
|
||||
@renderResponseCountAndPagination(
|
||||
if @isQuestion()
|
||||
then data["content"]["non_endorsed_resp_total"]
|
||||
else data["content"]["resp_total"]
|
||||
)
|
||||
@trigger "thread:responses:rendered"
|
||||
@loadedResponses = true
|
||||
error: (xhr) =>
|
||||
if xhr.status == 404
|
||||
DiscussionUtil.discussionAlert(
|
||||
@@ -75,16 +138,24 @@ if Backbone?
|
||||
)
|
||||
|
||||
loadInitialResponses: () ->
|
||||
@loadResponses(INITIAL_RESPONSE_PAGE_SIZE, @$el.find(".responses"), true)
|
||||
@loadResponses(INITIAL_RESPONSE_PAGE_SIZE, @$el.find(".js-response-list"), true)
|
||||
|
||||
renderResponseCountAndPagination: (responseTotal) =>
|
||||
if @isQuestion() && @markedAnswers.length != 0
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s other response",
|
||||
"%(numResponses)s other responses",
|
||||
responseTotal
|
||||
)
|
||||
else
|
||||
responseCountFormat = ngettext(
|
||||
"%(numResponses)s response",
|
||||
"%(numResponses)s responses",
|
||||
responseTotal
|
||||
)
|
||||
@$el.find(".response-count").html(
|
||||
interpolate(
|
||||
ngettext(
|
||||
"%(numResponses)s response",
|
||||
"%(numResponses)s responses",
|
||||
responseTotal
|
||||
),
|
||||
responseCountFormat,
|
||||
{numResponses: responseTotal},
|
||||
true
|
||||
)
|
||||
@@ -126,17 +197,17 @@ if Backbone?
|
||||
loadMoreButton.click((event) => @loadResponses(responseLimit, loadMoreButton))
|
||||
responsePagination.append(loadMoreButton)
|
||||
|
||||
renderResponse: (response) =>
|
||||
renderResponseToList: (response, listSelector, options) =>
|
||||
response.set('thread', @model)
|
||||
view = new ThreadResponseView(model: response)
|
||||
view = new ThreadResponseView($.extend({model: response}, options))
|
||||
view.on "comment:add", @addComment
|
||||
view.on "comment:endorse", @endorseThread
|
||||
view.render()
|
||||
@$el.find(".responses").append(view.el)
|
||||
@$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()
|
||||
@@ -150,9 +221,8 @@ if Backbone?
|
||||
addComment: =>
|
||||
@model.comment()
|
||||
|
||||
endorseThread: (endorsed) =>
|
||||
is_endorsed = @$el.find(".is-endorsed").length
|
||||
@model.set 'endorsed', is_endorsed
|
||||
endorseThread: =>
|
||||
@model.set 'endorsed', @$el.find(".action-answer.is-checked").length > 0
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
@@ -162,7 +232,7 @@ if Backbone?
|
||||
@setWmdContent("reply-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id"))
|
||||
comment.set('thread', @model.get('thread'))
|
||||
@renderResponse(comment)
|
||||
@renderResponseToList(comment, ".js-response-list")
|
||||
@model.addComment()
|
||||
@renderAddResponseButton()
|
||||
|
||||
@@ -209,6 +279,7 @@ if Backbone?
|
||||
@model.set
|
||||
title: newTitle
|
||||
body: newBody
|
||||
@model.unset("abbreviatedBody")
|
||||
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@@ -232,9 +303,6 @@ if Backbone?
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
|
||||
getShowViewClass: () ->
|
||||
return DiscussionThreadShowView
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@@ -242,8 +310,7 @@ if Backbone?
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
showViewClass = @getShowViewClass()
|
||||
@showView = new showViewClass(model: @model)
|
||||
@showView = new DiscussionThreadShowView({model: @model, mode: @mode})
|
||||
@showView.bind "thread:_delete", @_delete
|
||||
@showView.bind "thread:edit", @edit
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadInlineView extends DiscussionThreadView
|
||||
expanded = false
|
||||
events:
|
||||
"click .discussion-submit-post": "submitComment"
|
||||
"click .expand-post": "expandPost"
|
||||
"click .collapse-post": "collapsePost"
|
||||
"click .add-response-btn": "scrollToAddResponse"
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".discussion-article").children(".local")
|
||||
if not @$local.length
|
||||
@$local = @$el
|
||||
@$delegateElement = @$local
|
||||
|
||||
renderTemplate: () ->
|
||||
if @model.has('group_id')
|
||||
@template = DiscussionUtil.getTemplate("_inline_thread_cohorted")
|
||||
else
|
||||
@template = DiscussionUtil.getTemplate("_inline_thread")
|
||||
|
||||
if not @model.has('abbreviatedBody')
|
||||
@abbreviateBody()
|
||||
params = @model.toJSON()
|
||||
Mustache.render(@template, params)
|
||||
|
||||
render: () ->
|
||||
super()
|
||||
@$el.find('.post-extended-content').hide()
|
||||
@$el.find('.collapse-post').hide()
|
||||
|
||||
getShowViewClass: () ->
|
||||
return DiscussionThreadInlineShowView
|
||||
|
||||
loadInitialResponses: () ->
|
||||
if @expanded
|
||||
super()
|
||||
|
||||
abbreviateBody: ->
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
|
||||
@model.set('abbreviatedBody', abbreviated)
|
||||
|
||||
expandPost: (event) =>
|
||||
@$el.addClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('body'))
|
||||
@showView.convertMath()
|
||||
@$el.find('.expand-post').css('display', 'none')
|
||||
@$el.find('.collapse-post').css('display', 'block')
|
||||
@$el.find('.post-extended-content').show()
|
||||
if not @expanded
|
||||
@expanded = true
|
||||
@loadInitialResponses()
|
||||
|
||||
collapsePost: (event) ->
|
||||
curScroll = $(window).scrollTop()
|
||||
postTop = @$el.offset().top
|
||||
if postTop < curScroll
|
||||
$('html, body').animate({scrollTop: postTop})
|
||||
@$el.removeClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
|
||||
@showView.convertMath()
|
||||
@$el.find('.expand-post').css('display', 'block')
|
||||
@$el.find('.collapse-post').css('display', 'none')
|
||||
@$el.find('.post-extended-content').hide()
|
||||
|
||||
createEditView: () ->
|
||||
super()
|
||||
@editView.bind "thread:update", @abbreviateBody
|
||||
@@ -10,72 +10,51 @@ if Backbone?
|
||||
@topicId = options.topicId
|
||||
|
||||
render: () ->
|
||||
context = _.clone(@course_settings.attributes)
|
||||
_.extend(context, {
|
||||
cohort_options: @getCohortOptions(),
|
||||
mode: @mode,
|
||||
form_id: @mode + (if @topicId then "-" + @topicId else "")
|
||||
})
|
||||
context.topics_html = @renderCategoryMap(@course_settings.get("category_map")) if @mode is "tab"
|
||||
@$el.html(_.template($("#new-post-template").html(), context))
|
||||
|
||||
if @mode is "tab"
|
||||
@$el.html(
|
||||
_.template(
|
||||
$("#new-post-tab-template").html(), {
|
||||
topic_dropdown_html: @getTopicDropdownHTML(),
|
||||
options_html: @getOptionsHTML(),
|
||||
editor_html: @getEditorHTML()
|
||||
}
|
||||
)
|
||||
)
|
||||
# set up the topic dropdown in tab mode
|
||||
@dropdownButton = @$(".topic_dropdown_button")
|
||||
@topicMenu = @$(".topic_menu_wrapper")
|
||||
@menuOpen = @dropdownButton.hasClass('dropped')
|
||||
@topicId = @$(".topic").first().data("discussion_id")
|
||||
@topicText = @getFullTopicName(@$(".topic").first())
|
||||
$('.choose-cohort').hide() unless @$(".topic_menu li a").first().is("[cohorted=true]")
|
||||
@setSelectedTopic()
|
||||
else # inline
|
||||
@$el.html(
|
||||
_.template(
|
||||
$("#new-post-inline-template").html(), {
|
||||
options_html: @getOptionsHTML(),
|
||||
editor_html: @getEditorHTML()
|
||||
}
|
||||
)
|
||||
)
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
|
||||
@dropdownButton = @$(".post-topic-button")
|
||||
@topicMenu = @$(".topic-menu-wrapper")
|
||||
@hideTopicDropdown()
|
||||
@setTopic(@$("a.topic-title").first())
|
||||
|
||||
getTopicDropdownHTML: () ->
|
||||
# populate the category menu (topic dropdown)
|
||||
_renderCategoryMap = (map) ->
|
||||
category_template = _.template($("#new-post-menu-category-template").html())
|
||||
entry_template = _.template($("#new-post-menu-entry-template").html())
|
||||
html = ""
|
||||
for name in map.children
|
||||
if name of map.entries
|
||||
entry = map.entries[name]
|
||||
html += entry_template({text: name, id: entry.id, is_cohorted: entry.is_cohorted})
|
||||
else # subcategory
|
||||
html += category_template({text: name, entries: _renderCategoryMap(map.subcategories[name])})
|
||||
html
|
||||
topics_html = _renderCategoryMap(@course_settings.get("category_map"))
|
||||
_.template($("#new-post-topic-dropdown-template").html(), {topics_html: topics_html})
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body"
|
||||
|
||||
getEditorHTML: () ->
|
||||
_.template($("#new-post-editor-template").html(), {})
|
||||
renderCategoryMap: (map) ->
|
||||
category_template = _.template($("#new-post-menu-category-template").html())
|
||||
entry_template = _.template($("#new-post-menu-entry-template").html())
|
||||
html = ""
|
||||
for name in map.children
|
||||
if name of map.entries
|
||||
entry = map.entries[name]
|
||||
html += entry_template({text: name, id: entry.id, is_cohorted: entry.is_cohorted})
|
||||
else # subcategory
|
||||
html += category_template({text: name, entries: @renderCategoryMap(map.subcategories[name])})
|
||||
html
|
||||
|
||||
getOptionsHTML: () ->
|
||||
# cohort options?
|
||||
getCohortOptions: () ->
|
||||
if @course_settings.get("is_cohorted") and DiscussionUtil.isStaff()
|
||||
user_cohort_id = $("#discussion-container").data("user-cohort-id")
|
||||
cohort_options = _.map @course_settings.get("cohorts"), (cohort) ->
|
||||
_.map @course_settings.get("cohorts"), (cohort) ->
|
||||
{value: cohort.id, text: cohort.name, selected: cohort.id==user_cohort_id}
|
||||
else
|
||||
cohort_options = null
|
||||
context = _.clone(@course_settings.attributes)
|
||||
context.cohort_options = cohort_options
|
||||
_.template($("#new-post-options-template").html(), context)
|
||||
null
|
||||
|
||||
events:
|
||||
"submit .new-post-form": "createPost"
|
||||
"click .topic_dropdown_button": "toggleTopicDropdown"
|
||||
"click .topic_menu_wrapper": "setTopic"
|
||||
"click .topic_menu_search": "ignoreClick"
|
||||
"keyup .form-topic-drop-search-input": DiscussionFilter.filterDrop
|
||||
"submit .forum-new-post-form": "createPost"
|
||||
"click .post-topic-button": "toggleTopicDropdown"
|
||||
"click .topic-menu-wrapper": "handleTopicEvent"
|
||||
"click .topic-filter-label": "ignoreClick"
|
||||
"keyup .topic-filter-input": DiscussionFilter.filterDrop
|
||||
"change .post-option-input": "postOptionChange"
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to ignore clicks in the search field and stop propagation.
|
||||
@@ -83,15 +62,24 @@ if Backbone?
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
|
||||
postOptionChange: (event) ->
|
||||
$target = $(event.target)
|
||||
$optionElem = $target.closest(".post-option")
|
||||
if $target.is(":checked")
|
||||
$optionElem.addClass("is-enabled")
|
||||
else
|
||||
$optionElem.removeClass("is-enabled")
|
||||
|
||||
createPost: (event) ->
|
||||
event.preventDefault()
|
||||
title = @$(".new-post-title").val()
|
||||
body = @$(".new-post-body").find(".wmd-input").val()
|
||||
group = @$(".new-post-group option:selected").attr("value")
|
||||
thread_type = @$(".post-type-input:checked").val()
|
||||
title = @$(".js-post-title").val()
|
||||
body = @$(".js-post-body").find(".wmd-input").val()
|
||||
group = @$(".js-group-select option:selected").attr("value")
|
||||
|
||||
anonymous = false || @$("input.discussion-anonymous").is(":checked")
|
||||
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
|
||||
follow = false || @$("input.discussion-follow").is(":checked")
|
||||
anonymous = false || @$(".js-anon").is(":checked")
|
||||
anonymous_to_peers = false || @$(".js-anon-peers").is(":checked")
|
||||
follow = false || @$(".js-follow").is(":checked")
|
||||
|
||||
url = DiscussionUtil.urlFor('create_thread', @topicId)
|
||||
|
||||
@@ -103,24 +91,27 @@ if Backbone?
|
||||
dataType: 'json'
|
||||
async: false # TODO when the rest of the stuff below is made to work properly..
|
||||
data:
|
||||
thread_type: thread_type
|
||||
title: title
|
||||
body: body
|
||||
anonymous: anonymous
|
||||
anonymous_to_peers: anonymous_to_peers
|
||||
auto_subscribe: follow
|
||||
group_id: group
|
||||
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
|
||||
error: DiscussionUtil.formErrorHandler(@$(".post-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
thread = new Thread response['content']
|
||||
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
|
||||
DiscussionUtil.clearFormErrors(@$(".post-errors"))
|
||||
@$el.hide()
|
||||
@$(".new-post-title").val("").attr("prev-text", "")
|
||||
@$(".new-post-body textarea").val("").attr("prev-text", "")
|
||||
@$(".js-post-title").val("").attr("prev-text", "")
|
||||
@$(".js-post-body textarea").val("").attr("prev-text", "")
|
||||
@$(".wmd-preview p").html("") # only line not duplicated in new post inline view
|
||||
@collection.add thread
|
||||
|
||||
|
||||
toggleTopicDropdown: (event) ->
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if @menuOpen
|
||||
@hideTopicDropdown()
|
||||
@@ -133,7 +124,6 @@ if Backbone?
|
||||
@topicMenu.show()
|
||||
$(".form-topic-drop-search-input").focus()
|
||||
|
||||
$("body").bind "keydown", @setActiveItem
|
||||
$("body").bind "click", @hideTopicDropdown
|
||||
|
||||
# Set here because 1) the window might get resized and things could
|
||||
@@ -146,28 +136,33 @@ if Backbone?
|
||||
@dropdownButton.removeClass('dropped')
|
||||
@topicMenu.hide()
|
||||
|
||||
$("body").unbind "keydown", @setActiveItem
|
||||
$("body").unbind "click", @hideTopicDropdown
|
||||
|
||||
setTopic: (event) ->
|
||||
$target = $(event.target)
|
||||
if $target.data('discussion_id')
|
||||
handleTopicEvent: (event) ->
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@setTopic($(event.target))
|
||||
|
||||
setTopic: ($target) ->
|
||||
if $target.data('discussion-id')
|
||||
@topicText = $target.html()
|
||||
@topicText = @getFullTopicName($target)
|
||||
@topicId = $target.data('discussion_id')
|
||||
@topicId = $target.data('discussion-id')
|
||||
@setSelectedTopic()
|
||||
if $target.is('[cohorted=true]')
|
||||
$('.choose-cohort').show();
|
||||
if $target.data("cohorted")
|
||||
$(".js-group-select").prop("disabled", false)
|
||||
else
|
||||
$('.choose-cohort').hide();
|
||||
$(".js-group-select").val("")
|
||||
$(".js-group-select").prop("disabled", true)
|
||||
@hideTopicDropdown()
|
||||
|
||||
setSelectedTopic: ->
|
||||
@dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>')
|
||||
@$(".js-selected-topic").html(@fitName(@topicText))
|
||||
|
||||
getFullTopicName: (topicElement) ->
|
||||
name = topicElement.html()
|
||||
topicElement.parents('ul').not('.topic_menu').each ->
|
||||
name = $(this).siblings('a').text() + ' / ' + name
|
||||
topicElement.parents('.topic-submenu').each ->
|
||||
name = $(this).siblings('.topic-title').text() + ' / ' + name
|
||||
return name
|
||||
|
||||
getNameWidth: (name) ->
|
||||
@@ -204,29 +199,3 @@ if Backbone?
|
||||
name = gettext("…") + " / " + rawName + " " + gettext("…")
|
||||
|
||||
return name
|
||||
|
||||
setActiveItem: (event) ->
|
||||
if event.which == 13
|
||||
$(".topic_menu_wrapper .focused").click()
|
||||
return
|
||||
if event.which != 40 && event.which != 38
|
||||
return
|
||||
event.preventDefault()
|
||||
|
||||
items = $.makeArray($(".topic_menu_wrapper a").not(".hidden"))
|
||||
index = items.indexOf($('.topic_menu_wrapper .focused')[0])
|
||||
|
||||
if event.which == 40
|
||||
index = Math.min(index + 1, items.length - 1)
|
||||
if event.which == 38
|
||||
index = Math.max(index - 1, 0)
|
||||
|
||||
$(".topic_menu_wrapper .focused").removeClass("focused")
|
||||
$(items[index]).addClass("focused")
|
||||
|
||||
itemTop = $(items[index]).parent().offset().top
|
||||
scrollTop = $(".topic_menu").scrollTop()
|
||||
itemFromTop = $(".topic_menu").offset().top - itemTop
|
||||
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
|
||||
scrollTarget = Math.max(scrollTop - itemFromTop - $(".topic_menu").height() + $(items[index]).height() + 20, scrollTarget)
|
||||
$(".topic_menu").scrollTop(scrollTarget)
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
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))
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@markAsStaff()
|
||||
@$el.find(".timeago").timeago()
|
||||
@convertMath()
|
||||
@addReplyLink()
|
||||
@@ -52,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
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
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)
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
class @ThreadResponseShowView extends DiscussionContentShowView
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
@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()
|
||||
@$el.find(".posted-details .timeago").timeago()
|
||||
@convertMath()
|
||||
@markAsStaff()
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
@@ -39,54 +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.can('can_endorse')
|
||||
return
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
data = { endorsed: not endorsed }
|
||||
@model.set('endorsed', not endorsed)
|
||||
@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"))
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderVote()
|
||||
@renderFlagged()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseView extends DiscussionContentView
|
||||
tagName: "li"
|
||||
className: "forum-response"
|
||||
|
||||
events:
|
||||
"click .discussion-submit-comment": "submitComment"
|
||||
@@ -9,7 +10,8 @@ if Backbone?
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
initialize: (options) ->
|
||||
@collapseComments = options.collapseComments
|
||||
@createShowView()
|
||||
|
||||
renderTemplate: ->
|
||||
@@ -65,6 +67,15 @@ if Backbone?
|
||||
collectComments(child)
|
||||
@model.get('comments').each collectComments
|
||||
comments.each (comment) => @renderComment(comment, false, null)
|
||||
if @collapseComments && comments.length
|
||||
@$(".comments").hide()
|
||||
@$(".action-show-comments").on("click", (event) =>
|
||||
event.preventDefault()
|
||||
@$(".action-show-comments").hide()
|
||||
@$(".comments").show()
|
||||
)
|
||||
else
|
||||
@$(".action-show-comments").hide()
|
||||
|
||||
renderComment: (comment) =>
|
||||
comment.set('thread', @model.get('thread'))
|
||||
@@ -155,6 +166,7 @@ if Backbone?
|
||||
@showView = new ThreadResponseShowView(model: @model)
|
||||
@showView.bind "response:_delete", @_delete
|
||||
@showView.bind "response:edit", @edit
|
||||
@showView.on "comment:endorse", => @trigger("comment:endorse")
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
@@ -91,75 +91,7 @@ window.parseQueryString = function(queryString) {
|
||||
return parameters
|
||||
};
|
||||
|
||||
// Check if the user recently enrolled in a course by looking at a referral URL
|
||||
window.checkRecentEnrollment = function(referrer) {
|
||||
var enrolledIn = null;
|
||||
|
||||
// Check if the referrer URL contains a query string
|
||||
if (referrer.indexOf("?") > -1) {
|
||||
referrerQueryString = referrer.split("?")[1];
|
||||
} else {
|
||||
referrerQueryString = "";
|
||||
}
|
||||
|
||||
if (referrerQueryString != "") {
|
||||
// Convert a non-empty query string into a key/value object
|
||||
var referrerParameters = window.parseQueryString(referrerQueryString);
|
||||
if ("course_id" in referrerParameters && "enrollment_action" in referrerParameters) {
|
||||
if (referrerParameters.enrollment_action == "enroll") {
|
||||
enrolledIn = referrerParameters.course_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enrolledIn
|
||||
};
|
||||
|
||||
window.assessUserSignIn = function(parameters, userID, email, username) {
|
||||
// Check if the user has logged in to enroll in a course - designed for when "Register" button registers users on click (currently, this could indicate a course registration when there may not have yet been one)
|
||||
var enrolledIn = window.checkRecentEnrollment(document.referrer);
|
||||
|
||||
// Check if the user has just registered
|
||||
if (parameters.signin == "initial") {
|
||||
window.trackAccountRegistration(enrolledIn, userID, email, username);
|
||||
} else {
|
||||
window.trackReturningUserSignIn(enrolledIn, userID, email, username);
|
||||
}
|
||||
};
|
||||
|
||||
window.trackAccountRegistration = function(enrolledIn, userID, email, username) {
|
||||
// Alias the user's anonymous history with the user's new identity (for Mixpanel)
|
||||
analytics.alias(userID);
|
||||
|
||||
// Map the user's activity to their newly assigned ID
|
||||
analytics.identify(userID, {
|
||||
email: email,
|
||||
username: username
|
||||
});
|
||||
|
||||
// Track the user's account creation
|
||||
analytics.track("edx.bi.user.account.registered", {
|
||||
category: "conversion",
|
||||
label: enrolledIn != null ? enrolledIn : "none"
|
||||
});
|
||||
};
|
||||
|
||||
window.trackReturningUserSignIn = function(enrolledIn, userID, email, username) {
|
||||
// Map the user's activity to their assigned ID
|
||||
analytics.identify(userID, {
|
||||
email: email,
|
||||
username: username
|
||||
});
|
||||
|
||||
// Track the user's sign in
|
||||
analytics.track("edx.bi.user.account.authenticated", {
|
||||
category: "conversion",
|
||||
label: enrolledIn != null ? enrolledIn : "none"
|
||||
});
|
||||
};
|
||||
|
||||
window.identifyUser = function(userID, email, username) {
|
||||
// If the signin parameter isn't present but the query string is non-empty, map the user's activity to their assigned ID
|
||||
analytics.identify(userID, {
|
||||
email: email,
|
||||
username: username
|
||||
|
||||
@@ -30,6 +30,7 @@ class ContentFactory(factory.Factory):
|
||||
|
||||
|
||||
class Thread(ContentFactory):
|
||||
thread_type = "discussion"
|
||||
anonymous = False
|
||||
anonymous_to_peers = False
|
||||
comments_count = 0
|
||||
@@ -87,7 +88,13 @@ class SingleThreadViewFixture(DiscussionContentFixture):
|
||||
|
||||
def addResponse(self, response, comments=[]):
|
||||
response['children'] = comments
|
||||
self.thread.setdefault('children', []).append(response)
|
||||
if self.thread["thread_type"] == "discussion":
|
||||
responseListAttr = "children"
|
||||
elif response["endorsed"]:
|
||||
responseListAttr = "endorsed_responses"
|
||||
else:
|
||||
responseListAttr = "non_endorsed_responses"
|
||||
self.thread.setdefault(responseListAttr, []).append(response)
|
||||
self.thread['comments_count'] += len(comments) + 1
|
||||
|
||||
def _get_comment_map(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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,10 +110,23 @@ 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()
|
||||
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"""
|
||||
return self._is_element_visible(".response_{} .action-show-comments".format(response_id))
|
||||
|
||||
def show_comments(self, response_id):
|
||||
"""Click the "show comments" link for a response"""
|
||||
self._find_within(".response_{} .action-show-comments".format(response_id)).first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.is_response_editor_visible(response_id),
|
||||
"Response edit started"
|
||||
lambda: self._is_element_visible(".response_{} .comments".format(response_id)),
|
||||
"Comments shown"
|
||||
).fulfill()
|
||||
|
||||
def is_add_comment_visible(self, response_id):
|
||||
@@ -108,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"
|
||||
@@ -120,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"""
|
||||
@@ -132,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"""
|
||||
@@ -269,7 +307,7 @@ class InlineDiscussionThreadPage(DiscussionThreadPage):
|
||||
|
||||
def expand(self):
|
||||
"""Clicks the link to expand the thread"""
|
||||
self._find_within(".expand-post").first.click()
|
||||
self._find_within(".forum-thread-expand").first.click()
|
||||
EmptyPromise(
|
||||
lambda: bool(self.get_response_total_text()),
|
||||
"Thread expanded"
|
||||
|
||||
@@ -144,6 +144,27 @@ class DiscussionTabSingleThreadTest(UniqueCourseTest, DiscussionResponsePaginati
|
||||
self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, thread_id) # pylint:disable=W0201
|
||||
self.thread_page.visit()
|
||||
|
||||
def test_marked_answer_comments(self):
|
||||
thread_id = "test_thread_{}".format(uuid4().hex)
|
||||
response_id = "test_response_{}".format(uuid4().hex)
|
||||
comment_id = "test_comment_{}".format(uuid4().hex)
|
||||
thread_fixture = SingleThreadViewFixture(
|
||||
Thread(id=thread_id, commentable_id=self.discussion_id, thread_type="question")
|
||||
)
|
||||
thread_fixture.addResponse(
|
||||
Response(id=response_id, endorsed=True),
|
||||
[Comment(id=comment_id)]
|
||||
)
|
||||
thread_fixture.push()
|
||||
self.setup_thread_page(thread_id)
|
||||
self.assertFalse(self.thread_page.is_comment_visible(comment_id))
|
||||
self.assertFalse(self.thread_page.is_add_comment_visible(response_id))
|
||||
self.assertTrue(self.thread_page.is_show_comments_visible(response_id))
|
||||
self.thread_page.show_comments(response_id)
|
||||
self.assertTrue(self.thread_page.is_comment_visible(comment_id))
|
||||
self.assertTrue(self.thread_page.is_add_comment_visible(response_id))
|
||||
self.assertFalse(self.thread_page.is_show_comments_visible(response_id))
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class DiscussionCommentDeletionTest(UniqueCourseTest):
|
||||
|
||||
Reference in New Issue
Block a user