diff --git a/common/static/coffee/spec/discussion/.gitignore b/common/static/coffee/spec/discussion/.gitignore deleted file mode 100644 index ac5223af30..0000000000 --- a/common/static/coffee/spec/discussion/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -!view/discussion_thread_edit_view_spec.js -!view/discussion_topic_menu_view_spec.js diff --git a/common/static/coffee/spec/discussion/content_spec.coffee b/common/static/coffee/spec/discussion/content_spec.coffee deleted file mode 100644 index 77b8fd41e2..0000000000 --- a/common/static/coffee/spec/discussion/content_spec.coffee +++ /dev/null @@ -1,115 +0,0 @@ -describe 'All Content', -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - - describe 'Staff and TA Content', -> - beforeEach -> - DiscussionUtil.loadRoles({"Moderator": [567], "Administrator": [567], "Community TA": [567]}) - - it 'anonymous thread should not include login role label', -> - anon_content = new Content - anon_content.initialize - expect(anon_content.get 'staff_authored').toBe false - expect(anon_content.get 'community_ta_authored').toBe false - - it 'general thread should include login role label', -> - anon_content = new Content { user_id: '567' } - anon_content.initialize - expect(anon_content.get 'staff_authored').toBe true - expect(anon_content.get 'community_ta_authored').toBe true - - describe 'Content', -> - beforeEach -> - @content = new Content { - id: '01234567', - user_id: '567', - course_id: 'edX/999/test', - body: 'this is some content', - abuse_flaggers: ['123'] - } - - it 'should exist', -> - expect(Content).toBeDefined() - - it 'is initialized correctly', -> - @content.initialize - expect(Content.contents['01234567']).toEqual @content - expect(@content.get 'id').toEqual '01234567' - expect(@content.get 'user_url').toEqual '/courses/edX/999/test/discussion/forum/users/567' - expect(@content.get 'children').toEqual [] - expect(@content.get 'comments').toEqual(jasmine.any(Comments)) - - it 'can update info', -> - @content.updateInfo { - ability: {'can_edit': true}, - voted: true, - subscribed: true - } - expect(@content.get 'ability').toEqual {'can_edit': true} - expect(@content.get 'voted').toEqual true - expect(@content.get 'subscribed').toEqual true - - it 'can be flagged for abuse', -> - @content.flagAbuse() - expect(@content.get 'abuse_flaggers').toEqual ['123', '567'] - - it 'can be unflagged for abuse', -> - temp_array = [] - temp_array.push(window.user.get('id')) - @content.set("abuse_flaggers",temp_array) - @content.unflagAbuse() - expect(@content.get 'abuse_flaggers').toEqual [] - - describe 'Comments', -> - beforeEach -> - @comment1 = new Comment {id: '123'} - @comment2 = new Comment {id: '345'} - - it 'can contain multiple comments', -> - myComments = new Comments - expect(myComments.length).toEqual 0 - myComments.add @comment1 - expect(myComments.length).toEqual 1 - myComments.add @comment2 - expect(myComments.length).toEqual 2 - - it 'returns results to the find method', -> - 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) - diff --git a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee deleted file mode 100644 index bea233e0b5..0000000000 --- a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee +++ /dev/null @@ -1,72 +0,0 @@ -class @DiscussionSpecHelper - # This is sad. We should avoid dependence on global vars. - @setUpGlobals = -> - DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []}) - window.$$course_id = "edX/999/test" - window.user = new DiscussionUser({username: "test_user", id: "567", upvoted_ids: []}) - DiscussionUtil.setUser(window.user) - - @makeTA = () -> - DiscussionUtil.roleIds["Community TA"].push(parseInt(DiscussionUtil.getUser().id)) - - @makeModerator = () -> - DiscussionUtil.roleIds["Moderator"].push(parseInt(DiscussionUtil.getUser().id)) - - @makeAjaxSpy = (fakeAjax) -> - spyOn($, "ajax").and.callFake( - (params) -> - fakeAjax(params) - {always: ->} - ) - - @makeEventSpy = () -> - jasmine.createSpyObj('event', ['preventDefault', 'target']) - - @makeCourseSettings = (is_cohorted=true) -> - new DiscussionCourseSettings( - category_map: - children: ['Test Topic', 'Other Topic'] - entries: - 'Test Topic': - is_cohorted: is_cohorted - id: 'test_topic' - 'Other Topic': - is_cohorted: is_cohorted - id: 'other_topic' - is_cohorted: is_cohorted - ) - - @setUnderscoreFixtures = -> - templateNames = [ - 'thread', 'thread-show', 'thread-edit', - 'thread-response', 'thread-response-show', 'thread-response-edit', - 'response-comment-show', 'response-comment-edit', - 'thread-list-item', 'discussion-home', 'search-alert', - 'new-post', 'thread-type', 'new-post-menu-entry', - 'new-post-menu-category', 'topic', 'post-user-display', - 'inline-discussion', 'pagination', 'user-profile', 'profile-thread' - ] - templateNamesNoTrailingTemplate = [ - 'forum-action-endorse', 'forum-action-answer', 'forum-action-follow', - 'forum-action-vote', 'forum-action-report', 'forum-action-pin', - 'forum-action-close', 'forum-action-edit', 'forum-action-delete', - 'forum-actions', - ] - - for templateName in templateNames - templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore') - appendSetFixtures($(' - """) - @threads = [ - DiscussionViewSpecHelper.makeThreadWithProps({ - id: "1", - title: "Thread1", - votes: {up_count: '20'}, - pinned: true, - comments_count: 1, - created_at: '2013-04-03T20:08:39Z', - }), - DiscussionViewSpecHelper.makeThreadWithProps({ - id: "2", - title: "Thread2", - votes: {up_count: '42'}, - comments_count: 2, - created_at: '2013-04-03T20:07:39Z', - }), - DiscussionViewSpecHelper.makeThreadWithProps({ - id: "3", - title: "Thread3", - votes: {up_count: '12'}, - comments_count: 3, - created_at: '2013-04-03T20:06:39Z', - }), - DiscussionViewSpecHelper.makeThreadWithProps({ - id: "4", - title: "Thread4", - votes: {up_count: '25'}, - comments_count: 0, - pinned: true, - created_at: '2013-04-03T20:05:39Z', - }), - ] - deferred = $.Deferred() - spyOn($, "ajax").and.returnValue(deferred); - - @discussion = new Discussion([]) - @view = new DiscussionThreadListView( - collection: @discussion, - el: $("#fixture-element"), - courseSettings: new DiscussionCourseSettings({is_cohorted: true}) - ) - @view.render() - - setupAjax = (callback) -> - $.ajax.and.callFake( - (params) => - if callback - callback(params) - params.success({discussion_data: [], page: 1, num_pages: 1}) - {always: ->} - ) - - renderSingleThreadWithProps = (props) -> - makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render() - - makeView = (discussion) -> - return new DiscussionThreadListView( - el: $("#fixture-element"), - collection: discussion, - courseSettings: new DiscussionCourseSettings({is_cohorted: true}) - ) - - expectFilter = (filterVal) -> - $.ajax.and.callFake((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() - ) - - describe "cohort selector", -> - it "should not be visible to students", -> - expect(@view.$(".forum-nav-filter-cohort-control:visible")).not.toExist() - - it "should allow moderators to select visibility", -> - DiscussionSpecHelper.makeModerator() - @view.render() - expectedGroupId = null - setupAjax((params) => expect(params.data.group_id).toEqual(expectedGroupId)) - _.each( - [ - {val: "", expectedGroupId: undefined}, - {val: "1", expectedGroupId: "1"}, - {val: "2", expectedGroupId: "2"} - ], - (optionInfo) => - expectedGroupId = optionInfo.expectedGroupId - @view.$(".forum-nav-filter-cohort-control").val(optionInfo.val).change() - expect($.ajax).toHaveBeenCalled() - $.ajax.calls.reset() - ) - - 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(4) - expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()).toEqual(sort_order[0]) - expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text()).toEqual(sort_order[1]) - expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text()).toEqual(sort_order[2]) - expect(view.$el.find(".forum-nav-thread:nth-child(4) .forum-nav-thread-title").text()).toEqual(sort_order[3]) - expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type) - - describe "thread rendering should be correct", -> - checkRender = (threads, type, sort_order) -> - discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: type}) - view = makeView(discussion) - view.render() - checkThreadsOrdering(view, sort_order, type) - expect(view.$el.find(".forum-nav-thread-comments-count:visible").length).toEqual(if type == "votes" then 0 else 4) - expect(view.$el.find(".forum-nav-thread-votes-count:visible").length).toEqual(if type == "votes" then 4 else 0) - if type == "votes" - expect( - _.map( - view.$el.find(".forum-nav-thread-votes-count"), - (element) -> $(element).text().trim() - ) - ).toEqual(["+25 votes", "+20 votes", "+42 votes", "+12 votes"]) - - it "with sort preference activity", -> - checkRender(@threads, "activity", ["Thread1", "Thread2", "Thread3", "Thread4"]) - - it "with sort preference votes", -> - checkRender(@threads, "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]) - - it "with sort preference comments", -> - checkRender(@threads, "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]) - - describe "Sort change should be correct", -> - changeSorting = (threads, selected_type, new_type, sort_order) -> - discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: selected_type}) - view = makeView(discussion) - view.render() - sortControl = view.$el.find(".forum-nav-sort-control") - expect(sortControl.val()).toEqual(selected_type) - sorted_threads = [] - if new_type == 'activity' - sorted_threads = [threads[0], threads[3], threads[1], threads[2]] - else if new_type == 'comments' - sorted_threads = [threads[0], threads[3], threads[2], threads[1]] - else if new_type == 'votes' - sorted_threads = [threads[3], threads[0], threads[1], threads[2]] - $.ajax.and.callFake((params) => - params.success( - {"discussion_data":sorted_threads, page:1, num_pages:1} - ) - {always: ->} - ) - sortControl.val(new_type).change() - expect($.ajax).toHaveBeenCalled() - checkThreadsOrdering(view, sort_order, new_type) - - it "with sort preference activity", -> - changeSorting(@threads, "comments", "activity", ["Thread1", "Thread4", "Thread3", "Thread2"]) - - it "with sort preference votes", -> - changeSorting(@threads, "activity", "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]) - - it "with sort preference comments", -> - changeSorting(@threads, "votes", "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]) - - describe "search alerts", -> - - testAlertMessages = (expectedMessages) -> - expect($(".search-alert .message").map( -> - $(@).html() - ).get()).toEqual(expectedMessages) - - it "renders and removes search alerts", -> - testAlertMessages [] - foo = @view.addSearchAlert("foo") - testAlertMessages ["foo"] - bar = @view.addSearchAlert("bar") - testAlertMessages ["foo", "bar"] - @view.removeSearchAlert(foo.cid) - testAlertMessages ["bar"] - @view.removeSearchAlert(bar.cid) - testAlertMessages [] - - it "clears all search alerts", -> - @view.addSearchAlert("foo") - @view.addSearchAlert("bar") - @view.addSearchAlert("baz") - testAlertMessages ["foo", "bar", "baz"] - @view.clearSearchAlerts() - testAlertMessages [] - - describe "search spell correction", -> - - beforeEach -> - spyOn(@view, "searchForUser") - - testCorrection = (view, correctedText) -> - spyOn(view, "addSearchAlert") - $.ajax.and.callFake( - (params) => - params.success( - {discussion_data: [], page: 42, num_pages: 99, corrected_text: correctedText}, 'success' - ) - {always: ->} - ) - view.searchFor("dummy") - expect($.ajax).toHaveBeenCalled() - - it "adds a search alert when an alternate term was searched", -> - testCorrection(@view, "foo") - expect(@view.addSearchAlert.calls.count()).toEqual(1) - expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/foo/) - - it "does not add a search alert when no alternate term was searched", -> - testCorrection(@view, null) - expect(@view.addSearchAlert.calls.count()).toEqual(1) - expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/no threads matched/i) - - it "clears search alerts when a new search is performed", -> - spyOn(@view, "clearSearchAlerts") - spyOn(DiscussionUtil, "safeAjax") - @view.searchFor("dummy") - expect(@view.clearSearchAlerts).toHaveBeenCalled() - - it "clears search alerts when the underlying collection changes", -> - spyOn(@view, "clearSearchAlerts") - spyOn(@view, "renderThread") - @view.collection.trigger("change", new Thread({id: 1})) - expect(@view.clearSearchAlerts).toHaveBeenCalled() - - describe "Search events", -> - - it "perform search when enter pressed inside search textfield", -> - setupAjax() - spyOn(@view, "searchFor") - @view.$el.find(".forum-nav-search-input").trigger($.Event("keydown", {which: 13})) - expect(@view.searchFor).toHaveBeenCalled() - - it "perform search when search icon is clicked", -> - setupAjax() - spyOn(@view, "searchFor") - @view.$el.find(".fa-search").click() - expect(@view.searchFor).toHaveBeenCalled() - - describe "username search", -> - - it "makes correct ajax calls", -> - $.ajax.and.callFake( - (params) => - expect(params.data.username).toEqual("testing-username") - expect(params.url.path()).toEqual(DiscussionUtil.urlFor("users")) - params.success( - {users: []}, 'success' - ) - {always: ->} - ) - @view.searchForUser("testing-username") - expect($.ajax).toHaveBeenCalled() - - setAjaxResults = (threadSuccess, userResult) -> - # threadSuccess is a boolean indicating whether the thread search ajax call should succeed - # userResult is the value that should be returned as data from the username search ajax call - $.ajax.and.callFake( - (params) => - if params.data.text and threadSuccess - params.success( - {discussion_data: [], page: 42, num_pages: 99, corrected_text: "dummy"}, - "success" - ) - else if params.data.username - params.success( - {users: userResult}, - "success" - ) - {always: ->} - ) - - it "gets called after a thread search succeeds", -> - spyOn(@view, "searchForUser").and.callThrough() - setAjaxResults(true, []) - @view.searchFor("gizmo") - expect(@view.searchForUser).toHaveBeenCalled() - expect($.ajax.calls.mostRecent().args[0].data.username).toEqual("gizmo") - - it "does not get called after a thread search fails", -> - spyOn(@view, "searchForUser").and.callThrough() - setAjaxResults(false, []) - @view.searchFor("gizmo") - expect(@view.searchForUser).not.toHaveBeenCalled() - - it "adds a search alert when an username was matched", -> - spyOn(@view, "addSearchAlert") - setAjaxResults(true, [{username: "gizmo", id: "1"}]) - @view.searchForUser("dummy") - expect($.ajax).toHaveBeenCalled() - expect(@view.addSearchAlert).toHaveBeenCalled() - expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/gizmo/) - - it "does not add a search alert when no username was matched", -> - spyOn(@view, "addSearchAlert") - setAjaxResults(true, []) - @view.searchForUser("dummy") - expect($.ajax).toHaveBeenCalled() - expect(@view.addSearchAlert).not.toHaveBeenCalled() - - describe "post type renders correctly", -> - it "for discussion", -> - renderSingleThreadWithProps({thread_type: "discussion"}) - expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-comments") - expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion") - - it "for answered question", -> - renderSingleThreadWithProps({thread_type: "question", endorsed: true}) - expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-check-square-o") - 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("fa-question") - expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question") - - describe "post labels render correctly", -> - beforeEach -> - @moderatorId = "42" - @administratorId = "43" - @communityTaId = "44" - DiscussionUtil.loadRoles({ - "Moderator": [parseInt(@moderatorId)], - "Administrator": [parseInt(@administratorId)], - "Community TA": [parseInt(@communityTaId)], - }) - - it "for pinned", -> - renderSingleThreadWithProps({pinned: true}) - expect($(".post-label-pinned").length).toEqual(1) - - it "for following", -> - renderSingleThreadWithProps({subscribed: true}) - expect($(".post-label-following").length).toEqual(1) - - it "for moderator", -> - renderSingleThreadWithProps({user_id: @moderatorId}) - expect($(".post-label-by-staff").length).toEqual(1) - - it "for administrator", -> - renderSingleThreadWithProps({user_id: @administratorId}) - expect($(".post-label-by-staff").length).toEqual(1) - - it "for community TA", -> - renderSingleThreadWithProps({user_id: @communityTaId}) - expect($(".post-label-by-community-ta").length).toEqual(1) - - it "when none should be present", -> - renderSingleThreadWithProps({}) - expect($(".forum-nav-thread-labels").length).toEqual(0) - - describe "browse menu", -> - afterEach -> - # Remove handler added to make browse menu disappear - $("body").unbind("click") - - expectBrowseMenuVisible = (isVisible) -> - expect($(".forum-nav-browse-menu:visible").length).toEqual(if isVisible then 1 else 0) - expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(if isVisible then 0 else 1) - - it "should not be visible by default", -> - expectBrowseMenuVisible(false) - - it "should show when header button is clicked", -> - $(".forum-nav-browse").click() - expectBrowseMenuVisible(true) - - describe "when shown", -> - beforeEach -> - $(".forum-nav-browse").click() - - it "should hide when header button is clicked", -> - $(".forum-nav-browse").click() - expectBrowseMenuVisible(false) - - it "should hide when a click outside the menu occurs", -> - $(".forum-nav-search-input").click() - expectBrowseMenuVisible(false) - - it "should hide when a search is executed", -> - setupAjax() - $(".forum-nav-search-input").trigger($.Event("keydown", {which: 13})) - expectBrowseMenuVisible(false) - - it "should hide when a category is clicked", -> - $(".forum-nav-browse-title")[0].click() - expectBrowseMenuVisible(false) - - it "should still be shown when filter input is clicked", -> - $(".forum-nav-browse-filter-input").click() - expectBrowseMenuVisible(true) - - describe "filtering", -> - checkFilter = (filterText, expectedItems) -> - $(".forum-nav-browse-filter-input").val(filterText).keyup() - visibleItems = $(".forum-nav-browse-title:visible").map( - (i, elem) -> $(elem).text() - ).get() - expect(visibleItems).toEqual(expectedItems) - - it "should be case-insensitive", -> - checkFilter("other", ["Other Category"]) - - it "should match partial words", -> - checkFilter("ateg", ["Other Category"]) - - it "should show ancestors and descendants of matches", -> - checkFilter("Target", ["Parent", "Target", "Child"]) - - it "should handle multiple words regardless of order", -> - checkFilter("Following Posts", ["Posts I'm Following"]) - - it "should handle multiple words in different depths", -> - checkFilter("Parent Child", ["Parent", "Target", "Child"]) - - describe "selecting an item", -> - it "should clear the search box", -> - setupAjax() - $(".forum-nav-search-input").val("foobar") - $(".forum-nav-browse-menu-following .forum-nav-browse-title").click() - expect($(".forum-nav-search-input").val()).toEqual("") - - it "should change the button text", -> - setupAjax() - $(".forum-nav-browse-menu-following .forum-nav-browse-title").click() - expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following") - - it "should show/hide the cohort selector", -> - DiscussionSpecHelper.makeModerator() - @view.render() - setupAjax() - _.each( - [ - {selector: ".forum-nav-browse-menu-all", cohortVisibility: true}, - {selector: ".forum-nav-browse-menu-following", cohortVisibility: false}, - { - selector: ".forum-nav-browse-menu-item:has(.forum-nav-browse-menu-item .forum-nav-browse-menu-item)", - cohortVisibility: false - }, - {selector: "[data-discussion-id=child]", cohortVisibility: false}, - {selector: "[data-discussion-id=other]", cohortVisibility: true} - ], - (itemInfo) => - @view.$("#{itemInfo.selector} > .forum-nav-browse-title").click() - expect(@view.$(".forum-nav-filter-cohort").is(":visible")).toEqual(itemInfo.cohortVisibility) - ) - - testSelectionRequest = (callback, itemText) -> - setupAjax(callback) - $(".forum-nav-browse-title:contains(#{itemText})").click() - expect($.ajax).toHaveBeenCalled() - - it "should get all discussions", -> - testSelectionRequest( - (params) -> expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads")), - "All" - ) - - it "should get followed threads", -> - testSelectionRequest( - (params) -> - expect(params.url.path()).toEqual( - DiscussionUtil.urlFor("followed_threads", window.user.id) - ) - , - "Following" - ) - expect($.ajax.calls.mostRecent().args[0].data.group_id).toBeUndefined(); - - it "should get threads for the selected leaf", -> - testSelectionRequest( - (params) -> - expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search")) - expect(params.data.commentable_ids).toEqual("child") - , - "Child" - ) - - it "should get threads for children of the selected intermediate node", -> - testSelectionRequest( - (params) -> - expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search")) - expect(params.data.commentable_ids).toEqual("child,sibling") - , - "Parent" - ) diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee deleted file mode 100644 index 48572f67ae..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -describe "DiscussionThreadProfileView", -> - - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - @threadData = { - id: "1", - body: "dummy body", - discussion: new Discussion() - abuse_flaggers: [], - commentable_id: 'dummy_discussion', - votes: {up_count: "42"}, - created_at: "2014-09-09T20:11:08Z" - } - @imageTag = '' - window.MathJax = { Hub: { Queue: -> } } - - makeView = (thread) -> - view = new DiscussionThreadProfileView(model: thread) - spyConvertMath(view) - return view - - makeThread = (threadData) -> - thread = new Thread(threadData) - thread.discussion = new Discussion() - return thread - - spyConvertMath = (view) -> - spyOn(view, "convertMath").and.callFake( -> - @model.set('markdownBody', @model.get('body')) - ) - - checkPostWithImages = (numberOfImages, truncatedText, threadData, imageTag) -> - expectedHtml = '

' - threadData.body = '

' - testText = '' - expectedText = '' - - if truncatedText - testText = new Array(100).join('test ') - expectedText = testText.substring(0, 139)+ '…' - else - testText = 'Test body' - expectedText = 'Test body' - - for i in [0..numberOfImages-1] - threadData.body = threadData.body + imageTag - if i == 0 - expectedHtml = expectedHtml + imageTag - else - expectedHtml = expectedHtml + 'image omitted' - - threadData.body = threadData.body + '' + testText + '

' - if numberOfImages > 1 - expectedHtml = expectedHtml + '' + expectedText + '

Some images in this post have been omitted

' - else - expectedHtml = expectedHtml + '' + expectedText + '

' - - view = makeView(makeThread(threadData)) - view.render() - expect(view.$el.find(".post-body").html()).toEqual(expectedHtml) - - checkBody = (truncated, view, threadData) -> - view.render() - if not truncated - expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody")) - expect(view.$el.find(".post-body").html()).toEqual(threadData.body) - else - expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody")) - expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body) - outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig,""); - outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted","") - outputHtmlStripped = outputHtmlStripped.replace("image omitted","") - inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig,""); - expectedOutput = inputHtmlStripped.substring(0, 139)+ '…' - expect(outputHtmlStripped).toEqual(expectedOutput) - expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0) - - describe "Body markdown should be correct", -> - - it "untruncated text without markdown body", -> - @threadData.body = "Test body" - view = makeView(makeThread(@threadData)) - checkBody(false, view, @threadData) - - it "truncated text without markdown body", -> - @threadData.body = new Array(100).join("test ") - view = makeView(makeThread(@threadData)) - checkBody(true, view, @threadData) - - it "untruncated text with markdown body", -> - @threadData.body = '

' + @imageTag + 'Google top search engine

' - view = makeView(makeThread(@threadData)) - checkBody(false, view, @threadData) - - it "truncated text with markdown body", -> - testText = new Array(100).join("test ") - @threadData.body = '

' + @imageTag + @imageTag + '' + testText + '

' - view = makeView(makeThread(@threadData)) - checkBody(true, view, @threadData) - - for numImages in [1, 2, 10] - for truncatedText in [true, false] - it "body with #{numImages} images and #{if truncatedText then "truncated" else "untruncated"} text", -> - checkPostWithImages(numImages, truncatedText, @threadData, @imageTag) - - it "check the thread retrieve url", -> - thread = makeThread(@threadData) - expect(thread.urlFor('retrieve')).toBe('/courses/edX/999/test/discussion/forum/dummy_discussion/threads/1') diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee deleted file mode 100644 index 34036d847b..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee +++ /dev/null @@ -1,159 +0,0 @@ -describe "DiscussionThreadShowView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - - @user = DiscussionUtil.getUser() - @threadData = { - id: "dummy", - 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}, - 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($("#fixture-element")) - spyOn(@view, "convertMath") - - describe "voting", -> - - it "renders the vote state correctly", -> - DiscussionViewSpecHelper.checkRenderVote(@view, @thread) - - it "votes correctly via click", -> - DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("click")) - - it "votes correctly via spacebar", -> - DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("keydown", {which: 32})) - - it "unvotes correctly via click", -> - DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("click")) - - it "unvotes correctly via spacebar", -> - DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("keydown", {which: 32})) - - describe "pinning", -> - - expectPinnedRendered = (view, model) -> - pinned = model.get('pinned') - button = view.$el.find(".action-pin") - expect(button.hasClass("is-checked")).toBe(pinned) - expect(button.attr("aria-checked")).toEqual(pinned.toString()) - - it "renders the pinned state correctly", -> - @view.render() - expectPinnedRendered(@view, @thread) - @thread.set('pinned', false) - @view.render() - expectPinnedRendered(@view, @thread) - @thread.set('pinned', true) - @view.render() - expectPinnedRendered(@view, @thread) - - it "exposes the pinning control only to authorized users", -> - @thread.updateInfo({ability: {can_openclose: false}}) - @view.render() - expect(@view.$el.find(".action-pin").closest(".is-hidden")).toExist() - @thread.updateInfo({ability: {can_openclose: true}}) - @view.render() - expect(@view.$el.find(".action-pin").closest(".is-hidden")).not.toExist() - - it "handles events correctly", -> - @view.render() - DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".action-pin") - - describe "labels", -> - - expectOneElement = (view, selector, visible=true) => - view.render() - elements = view.$el.find(selector) - expect(elements.length).toEqual(1) - if visible - expect(elements).not.toHaveClass("is-hidden") - else - expect(elements).toHaveClass("is-hidden") - - it 'displays the closed label when appropriate', -> - expectOneElement(@view, '.post-label-closed', false) - @thread.set('closed', true) - expectOneElement(@view, '.post-label-closed') - - it 'displays the pinned label when appropriate', -> - expectOneElement(@view, '.post-label-pinned', false) - @thread.set('pinned', true) - expectOneElement(@view, '.post-label-pinned') - - it 'displays the reported label when appropriate for a non-staff user', -> - expectOneElement(@view, '.post-label-reported', false) - # flagged by current user - should be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]) - expectOneElement(@view, '.post-label-reported') - # flagged by some other user but not the current one - should not be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]) - expectOneElement(@view, '.post-label-reported', false) - - it 'displays the reported label when appropriate for a flag moderator', -> - DiscussionSpecHelper.makeModerator() - expectOneElement(@view, '.post-label-reported', false) - # flagged by current user - should be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]) - expectOneElement(@view, '.post-label-reported') - # flagged by some other user but not the current one - should still be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]) - expectOneElement(@view, '.post-label-reported') - - describe "author display", -> - - beforeEach -> - @thread.set('user_url', 'test_user_url') - - checkUserLink = (element, is_ta, is_staff) -> - expect(element.find('a.username').length).toEqual(1) - expect(element.find('a.username').text()).toEqual('test_user') - expect(element.find('a.username').attr('href')).toEqual('test_user_url') - expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0) - expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0) - - it "renders correctly for a student-authored thread", -> - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, false, false) - - it "renders correctly for a community TA-authored thread", -> - @thread.set('community_ta_authored', true) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, true, false) - - it "renders correctly for a staff-authored thread", -> - @thread.set('staff_authored', true) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, false, true) - - it "renders correctly for an anonymously-authored thread", -> - @thread.set('username', null) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - expect($el.find('a.username').length).toEqual(0) - expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/) - - describe "cohorting", -> - it "renders correctly for an uncohorted thread", -> - @view.render() - expect(@view.$('.group-visibility-label').text().trim()).toEqual( - 'This post is visible to everyone.' - ) - - it "renders correctly for a cohorted thread", -> - @thread.set('group_id', '1') - @thread.set('group_name', 'Mock Cohort') - @view.render() - expect(@view.$('.group-visibility-label').text().trim()).toEqual( - 'This post is visible only to Mock Cohort.' - ) diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee deleted file mode 100644 index 53904d9b87..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee +++ /dev/null @@ -1,410 +0,0 @@ -describe "DiscussionThreadView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - - jasmine.clock().install() - @threadData = DiscussionViewSpecHelper.makeThreadWithProps({}) - @thread = new Thread(@threadData) - @discussion = new Discussion(@thread) - deferred = $.Deferred(); - spyOn($, "ajax").and.returnValue(deferred); - # Avoid unnecessary boilerplate - spyOn(DiscussionThreadShowView.prototype, "convertMath") - spyOn(DiscussionContentView.prototype, "makeWmdEditor") - spyOn(DiscussionUtil, "makeWmdEditor") - spyOn(DiscussionUtil, "setWmdContent") - spyOn(ThreadResponseShowView.prototype, "convertMath") - - afterEach -> - $.ajax.calls.reset() - jasmine.clock().uninstall() - - renderWithContent = (view, content) -> - $.ajax.and.callFake((params) => - params.success( - createAjaxResponseJson(content, false), - 'success' - ) - {always: ->} - ) - view.render() - jasmine.clock().tick(100) - - renderWithTestResponses = (view, count, options) -> - renderWithContent( - view, - _.extend( - { - resp_total: count, - children: if count > 0 then (createTestResponseJson(index) for index in [1..count]) else [] - }, - options - ) - ) - - createTestResponseJson = (index) -> - { - user_id: window.user.id, - body: "Response " + index, - id: "id_" + index, - created_at: "2015-01-01T22:20:28Z" - } - - assertContentVisible = (view, selector, visible) -> - content = view.$el.find(selector) - expect(content.length).toBeGreaterThan(0) - content.each (i, elem) -> - expect($(elem).is(":visible")).toEqual(visible) - - 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) - - 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) - - createAjaxResponseJson = (content, can_act) -> - { - content: content, - annotated_content_info: { - ability: { - editable: can_act, - can_delete: can_act, - can_reply: can_act, - can_vote: can_act - } - } - } - - postResponse = (view, index) -> - testResponseJson = createTestResponseJson(index) - responseText = testResponseJson.body - spyOn(view, "getWmdContent").and.returnValue(responseText) - $.ajax.and.callFake((params) => - expect(params.type).toEqual("POST") - expect(params.data.body).toEqual(responseText) - params.success( - createAjaxResponseJson(testResponseJson, true), - 'success' - ) - {always: ->} - ) - view.$(".discussion-submit-post").click() - - describe "closed and open Threads", -> - - createDiscussionThreadView = (originallyClosed, mode) -> - threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed}) - thread = new Thread(threadData) - discussion = new Discussion(thread) - view = new DiscussionThreadView( - model: thread - el: $("#fixture-element") - mode: mode - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - renderWithTestResponses(view, 1) - if mode == "inline" - view.expand() - spyOn(DiscussionUtil, "updateWithUndo").and.callFake( - (model, updates, safeAjaxParams, errorMsg) -> - model.set(updates) - ) - view - - checkCommentForm = (originallyClosed, mode) -> - view = createDiscussionThreadView(originallyClosed, mode) - expect(view.$('.comment-form').closest('li').is(":visible")).toBe(not originallyClosed) - expect(view.$(".discussion-reply-new").is(":visible")).toBe(not originallyClosed) - view.$(".action-close").click() - expect(view.$('.comment-form').closest('li').is(":visible")).toBe(originallyClosed) - expect(view.$(".discussion-reply-new").is(":visible")).toBe(originallyClosed) - - checkVoteDisplay = (originallyClosed, mode) -> - view = createDiscussionThreadView(originallyClosed, mode) - expect(view.$('.thread-main-wrapper .action-vote').is(":visible")).toBe(not originallyClosed) - expect(view.$('.thread-main-wrapper .display-vote').is(":visible")).toBe(originallyClosed) - view.$(".action-close").click() - expect(view.$('.action-vote').is(":visible")).toBe(originallyClosed) - expect(view.$('.display-vote').is(":visible")).toBe(not originallyClosed) - - _.each(["tab", "inline"], (mode) => - it "Test that in #{mode} mode when a closed thread is opened the comment form is displayed", -> - checkCommentForm(true, mode) - - it "Test that in #{mode} mode when a open thread is closed the comment form is hidden", -> - checkCommentForm(false, mode) - - it "Test that in #{mode} mode when a closed thread is opened the vote button is displayed and vote count is hidden", -> - checkVoteDisplay(true, mode) - - it "Test that in #{mode} mode when a open thread is closed the vote button is hidden and vote count is displayed", -> - checkVoteDisplay(false, mode) - ) - - describe "tab mode", -> - beforeEach -> - @view = new DiscussionThreadView( - model: @thread - el: $("#fixture-element") - mode: "tab" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - describe "responses", -> - it "can post a first response", -> - # Initially render a test post (made by someone else) with zero responses - renderWithTestResponses(@view, 0) - postResponse(@view, 1) - expect(@view.$(".forum-response").length).toBe(1) - # At this point, there are 2 DiscussionContentViews, the main post and the response. - # Each an .action-edit button, but only 1 (the response) should be available. - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1) - - it "can post a second response", -> - # Initially render a test post (made by someone else) with a single response (made by the current learner) - renderWithTestResponses(@view, 1) - expect(@view.$(".forum-response").length).toBe(1) - # Post should not be editable, response should be - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1) - - # Now make a second response. Prior to TNL-3788, a bug would hide the edit button for the first response - postResponse(@view, 2) - expect(@view.$(".forum-response").length).toBe(2) - # Post should not be editable, responses should be - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(2) - - describe "response count and pagination", -> - it "correctly render for a thread with no responses", -> - renderWithTestResponses(@view, 0) - assertResponseCountAndPaginationCorrect(@view, "0 responses", null, null) - - it "correctly render for a thread with one response", -> - renderWithTestResponses(@view, 1) - assertResponseCountAndPaginationCorrect(@view, "1 response", "Showing all responses", null) - - it "correctly render for a thread with one additional page", -> - renderWithTestResponses(@view, 1, {resp_total: 2}) - assertResponseCountAndPaginationCorrect(@view, "2 responses", "Showing first response", "Load all responses") - - it "correctly render for a thread with multiple additional pages", -> - renderWithTestResponses(@view, 2, {resp_total: 111}) - assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses") - - describe "on clicking the load more button", -> - beforeEach -> - renderWithTestResponses(@view, 1, {resp_total: 5}) - assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing first response", "Load all responses") - - it "correctly re-render when all threads have loaded", -> - renderWithTestResponses(@view, 5, {resp_total: 5}) - @view.$el.find(".load-response-button").click() - assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing all responses", null) - - it "correctly re-render when one page remains", -> - renderWithTestResponses(@view, 3, {resp_total: 42}) - @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", -> - renderWithTestResponses(@view, 3, {resp_total: 111}) - @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" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - 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 "focus", -> - it "sends focus to the conversation when opened", (done) -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - @view.render() - @view.expand() - self = @ - jasmine.waitUntil(-> - # This is the implementation of "toBeFocused". However, simply calling that method - # with no wait seems to be flaky. - article = self.view.$el.find('.discussion-article') - return article[0] == article[0].ownerDocument.activeElement - ).then -> - done() - - 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.calls.reset() - - @view.expand() - expect($(".post-body").text()).toEqual(longBody) - expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled() - DiscussionThreadShowView.prototype.convertMath.calls.reset() - - @view.collapse() - expect($(".post-body").text()).toEqual(expectedAbbreviation) - expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled() - - it "strips script tags appropriately", -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - longMaliciousBody = new Array(100).join("\n") - @thread.set("body", longMaliciousBody) - maliciousAbbreviation = DiscussionUtil.abbreviateString(@thread.get('body'), 140) - - # The nodes' html should be different than the strings, but - # their texts should be the same, indicating that they've been - # properly escaped. To be safe, make sure the string " - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - @view.render() - @view.expand() - assertExpandedContentVisible(@view, true) - @view.edit() - assertContentVisible(@view, ".edit-post-body", true) - expect(@view.$el.find(".post-actions-list").length).toBe(0) - @view.closeEditView(DiscussionSpecHelper.makeEventSpy()) - expect(@view.$el.find(".edit-post-body").length).toBe(0) - assertContentVisible(@view, ".post-actions-list", true) - - describe "for question threads", -> - beforeEach -> - @thread.set("thread_type", "question") - @view = new DiscussionThreadView( - model: @thread - el: $("#fixture-element") - mode: "tab" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - generateContent = (idStart, idEnd) -> - _.map(_.range(idStart, idEnd), (i) -> createTestResponseJson(i)) - - renderTestCase = (view, numEndorsed, numNonEndorsed) -> - 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, - "#{numNonEndorsed} #{if numEndorsed then "other " else ""}#{if numNonEndorsed == 1 then "response" else "responses"}", - 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: generateContent(0, 2), - non_endorsed_responses: generateContent(3, 6), - non_endorsed_resp_total: 42 - } - ) - DiscussionViewSpecHelper.setNextResponseContent({ - # Add an endorsed response; it should be rendered - endorsed_responses: generateContent(0, 3), - non_endorsed_responses: generateContent(6, 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" - ) - - describe "post restrictions", -> - beforeEach -> - @thread.attributes.ability = _.extend(@thread.attributes.ability, { - can_report: false - can_vote: false - }) - @view = new DiscussionThreadView( - model: @thread - el: $("#fixture-element") - mode: "tab" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - it "doesn't show report option if can_report ability is disabled", -> - @view.render() - expect(@view.$el.find(".action-report").closest(".actions-item")).toHaveClass('is-hidden') - - it "doesn't show voting button if can_vote ability is disabled", -> - @view.render() - expect(@view.$el.find(".action-vote").closest(".actions-item")).toHaveClass('is-hidden') diff --git a/common/static/coffee/spec/discussion/view/discussion_user_profile_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_user_profile_view_spec.coffee deleted file mode 100644 index 98d47d7205..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_user_profile_view_spec.coffee +++ /dev/null @@ -1,221 +0,0 @@ -describe "DiscussionUserProfileView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - spyOn(DiscussionThreadProfileView.prototype, "render") - - makeThreads = (numThreads) -> - _.map(_.range(numThreads), (i) -> {id: i.toString(), body: "dummy body"}) - - makeView = (threads, page, numPages) -> - new DiscussionUserProfileView( - collection: threads - page: page - numPages: numPages - ) - - describe "thread rendering should be correct", -> - checkRender = (numThreads) -> - threads = makeThreads(numThreads) - view = makeView(threads, 1, 1) - expect(view.$(".discussion").children().length).toEqual(numThreads) - _.each(threads, (thread) -> expect(view.$("#thread_#{thread.id}").length).toEqual(1)) - - it "with no threads", -> - checkRender(0) - - it "with one thread", -> - checkRender(1) - - it "with several threads", -> - checkRender(5) - - describe "pagination rendering should be correct", -> - baseUri = URI(window.location) - - pageInfo = (page) -> {url: baseUri.clone().addSearch("page", page).toString(), number: page} - - checkRender = (params) -> - view = makeView([], params.page, params.numPages) - paginator = view.$(".discussion-paginator") - expect(paginator.find(".current-page").text()).toEqual(params["page"].toString()) - expect(paginator.find(".first-page").length).toBe(if params["first"] then 1 else 0); - expect(paginator.find(".previous-page").length).toBe(if params["previous"] then 1 else 0); - expect(paginator.find(".previous-ellipses").length).toBe(if params["leftdots"] then 1 else 0); - expect(paginator.find(".next-page").length).toBe(if params["next"] then 1 else 0); - expect(paginator.find(".next-ellipses").length).toBe(if params["rightdots"] then 1 else 0); - expect(paginator.find(".last-page").length).toBe(if params["last"] then 1 else 0); - - get_page_number = (element) => parseInt($(element).text()) - expect(_.map(paginator.find(".lower-page a"), get_page_number)).toEqual(params["lowPages"]) - expect(_.map(paginator.find(".higher-page a"), get_page_number)).toEqual(params["highPages"]) - - it "for one page", -> - checkRender( - page: 1 - numPages: 1 - previous: null - first: null - leftdots: false - lowPages: [] - highPages: [] - rightdots: false - last: null - next: null - ) - - it "for first page of three (max with no last)", -> - checkRender( - page: 1 - numPages: 3 - previous: null - first: null - leftdots: false - lowPages: [] - highPages: [2, 3] - rightdots: false - last: null - next: 2 - ) - - it "for first page of four (has last but no dots)", -> - checkRender( - page: 1 - numPages: 4 - previous: null - first: null - leftdots: false - lowPages: [] - highPages: [2, 3] - rightdots: false - last: 4 - next: 2 - ) - - it "for first page of five (has dots)", -> - checkRender( - page: 1 - numPages: 5 - previous: null - first: null - leftdots: false - lowPages: [] - highPages: [2, 3] - rightdots: true - last: 5 - next: 2 - ) - - it "for last page of three (max with no first)", -> - checkRender( - page: 3 - numPages: 3 - previous: 2 - first: null - leftdots: false - lowPages: [1, 2] - highPages: [] - rightdots: false - last: null - next: null - ) - - it "for last page of four (has first but no dots)", -> - checkRender( - page: 4 - numPages: 4 - previous: 3 - first: 1 - leftdots: false - lowPages: [2, 3] - highPages: [] - rightdots: false - last: null - next: null - ) - - it "for last page of five (has dots)", -> - checkRender( - page: 5 - numPages: 5 - previous: 4 - first: 1 - leftdots: true - lowPages: [3, 4] - highPages: [] - rightdots: false - last: null - next: null - ) - - it "for middle page of five (max with no first/last)", -> - checkRender( - page: 3 - numPages: 5 - previous: 2 - first: null - leftdots: false - lowPages: [1, 2] - highPages: [4, 5] - rightdots: false - last: null - next: 4 - ) - - it "for middle page of seven (has first/last but no dots)", -> - checkRender( - page: 4 - numPages: 7 - previous: 3 - first: 1 - leftdots: false - lowPages: [2, 3] - highPages: [5, 6] - rightdots: false - last: 7 - next: 5 - ) - - it "for middle page of nine (has dots)", -> - checkRender( - page: 5 - numPages: 9 - previous: 4 - first: 1 - leftdots: true - lowPages: [3, 4] - highPages: [6, 7] - rightdots: true - last: 9 - next: 6 - ) - - describe "pagination interaction", -> - beforeEach -> - @view = makeView(makeThreads(3), 1, 2) - deferred = $.Deferred(); - spyOn($, "ajax").and.returnValue(deferred); - - it "causes updated rendering", -> - $.ajax.and.callFake( - (params) => - params.success( - discussion_data: [{id: "on_page_42", body: "dummy body"}] - page: 42 - num_pages: 99 - ) - {always: ->} - ) - @view.$(".discussion-pagination a").first().click() - expect(@view.$(".current-page").text()).toEqual("42") - expect(@view.$(".last-page").text()).toEqual("99") - - it "handles AJAX errors", -> - spyOn(DiscussionUtil, "discussionAlert") - $.ajax.and.callFake( - (params) => - params.error() - {always: ->} - ) - @view.$(".discussion-pagination a").first().click() - expect(DiscussionUtil.discussionAlert).toHaveBeenCalled() diff --git a/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee b/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee deleted file mode 100644 index d3150d7dfd..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee +++ /dev/null @@ -1,98 +0,0 @@ -class @DiscussionViewSpecHelper - @makeThreadWithProps = (props) -> - # Minimal set of properties necessary for rendering - thread = { - id: "dummy_id", - thread_type: "discussion", - pinned: false, - endorsed: false, - votes: {up_count: '0'}, - read: false, - unread_comments_count: 0, - comments_count: 0, - abuse_flaggers: [], - body: "", - title: "dummy title", - created_at: "2014-08-18T01:02:03Z" - ability: { - can_delete: false, - can_reply: true, - can_vote: false, - editable: false, - } - } - $.extend(thread, props) - - @checkVoteClasses = (view) -> - view.render() - display_button = view.$el.find(".display-vote") - expect(display_button.hasClass("is-hidden")).toBe(true) - action_button = view.$el.find(".action-vote") - # Check that inline css is not applied to the ".action-vote" - expect(action_button).not.toHaveAttr('style','display: inline; '); - - @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(".vote-count").text()).toMatch("^#{model.get('votes').up_count} Votes?$") - expect(button.find(".sr.js-sr-vote-count").text()).toMatch("^there are currently #{model.get('votes').up_count} votes?$") - - @checkRenderVote = (view, model) -> - view.render() - DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user) - window.user.vote(model) - view.render() - DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user) - window.user.unvote(model) - view.render() - DiscussionViewSpecHelper.expectVoteRendered(view, model, window.user) - - triggerVoteEvent = (view, event, expectedUrl) -> - deferred = $.Deferred() - spyOn($, "ajax").and.callFake((params) => - expect(params.url.toString()).toEqual(expectedUrl) - return deferred - ) - view.render() - view.$el.find(".action-vote").trigger(event) - expect($.ajax).toHaveBeenCalled() - deferred.resolve() - - @checkUpvote = (view, model, user, event) -> - expect(model.id in user.get('upvoted_ids')).toBe(false) - initialVoteCount = model.get('votes').up_count - triggerVoteEvent(view, event, DiscussionUtil.urlFor("upvote_#{model.get('type')}", model.id) + "?ajax=1") - expect(model.id in user.get('upvoted_ids')).toBe(true) - expect(model.get('votes').up_count).toEqual(initialVoteCount + 1) - - @checkUnvote = (view, model, user, event) -> - user.vote(model) - expect(model.id in user.get('upvoted_ids')).toBe(true) - initialVoteCount = model.get('votes').up_count - triggerVoteEvent(view, event, DiscussionUtil.urlFor("undo_vote_for_#{model.get('type')}", model.id) + "?ajax=1") - expect(user.get('upvoted_ids')).toEqual([]) - expect(model.get('votes').up_count).toEqual(initialVoteCount - 1) - - @checkButtonEvents = (view, viewFunc, buttonSelector) -> - spy = spyOn(view, viewFunc) - button = view.$el.find(buttonSelector) - - button.click() - expect(spy).toHaveBeenCalled() - spy.calls.reset() - button.trigger($.Event("keydown", {which: 13})) - expect(spy).not.toHaveBeenCalled() - spy.calls.reset() - button.trigger($.Event("keydown", {which: 32})) - expect(spy).toHaveBeenCalled() - - @checkVoteButtonEvents = (view) -> - @checkButtonEvents(view, "toggleVote", ".action-vote") - - @setNextResponseContent = (content) -> - $.ajax.and.callFake( - (params) => - params.success({"content": content}) - {always: ->} - ) diff --git a/common/static/coffee/spec/discussion/view/new_post_view_spec.coffee b/common/static/coffee/spec/discussion/view/new_post_view_spec.coffee deleted file mode 100644 index fb9f244897..0000000000 --- a/common/static/coffee/spec/discussion/view/new_post_view_spec.coffee +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -describe "NewPostView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - window.$$course_id = "edX/999/test" - spyOn(DiscussionUtil, "makeWmdEditor").and.callFake( - ($content, $local, cls_identifier) -> - $local("." + cls_identifier).html("") - ) - @discussion = new Discussion([], {pages: 1}) - - checkVisibility = (view, expectedVisible, expectedDisabled, render) => - if render - view.render() - # Can also be undefined if the element does not exist. - expect(view.$('.group-selector-wrapper').is(":visible") or false).toEqual(expectedVisible) - disabled = view.$(".js-group-select").prop("disabled") or false - group_disabled = view.$('.group-selector-wrapper').hasClass('disabled') - if expectedVisible and !expectedDisabled - expect(disabled).toEqual(false) - expect(group_disabled).toEqual(false) - else if expectedDisabled - expect(disabled).toEqual(true) - expect(group_disabled).toEqual(true) - - describe "cohort selector", -> - beforeEach -> - @course_settings = new DiscussionCourseSettings({ - "category_map": { - "children": ["Topic", "General"], - "entries": { - "Topic": {"is_cohorted": true, "id": "topic"}, - "General": {"is_cohorted": false, "id": "general"} - } - }, - "allow_anonymous": false, - "allow_anonymous_to_peers": false, - "is_cohorted": true, - "cohorts": [ - {"id": 1, "name": "Cohort1"}, - {"id": 2, "name": "Cohort2"} - ] - }) - @view = new NewPostView( - el: $("#fixture-element"), - collection: @discussion, - course_settings: @course_settings, - is_commententable_cohorted: true, - mode: "tab" - ) - - it "is not visible to students", -> - checkVisibility(@view, false, false, true) - - it "allows TAs to see the cohort selector", -> - DiscussionSpecHelper.makeTA() - checkVisibility(@view, true, false, true) - - it "allows moderators to see the cohort selector", -> - DiscussionSpecHelper.makeModerator() - checkVisibility(@view, true, false, true) - - it "only enables the cohort selector when applicable", -> - DiscussionSpecHelper.makeModerator() - # We start on the cohorted discussion - checkVisibility(@view, true, false, true) - # Select the uncohorted topic - $('.topic-title:contains(General)').click() - # The menu should now be visible but disabled. - checkVisibility(@view, true, true, false) - # Select the cohorted topic again - $('.topic-title:contains(Topic)').click() - # It should be visible and enabled once more. - checkVisibility(@view, true, false, false) - - it "allows the user to make a cohort selection", -> - DiscussionSpecHelper.makeModerator() - @view.render() - expectedGroupId = null - DiscussionSpecHelper.makeAjaxSpy( - (params) -> expect(params.data.group_id).toEqual(expectedGroupId) - ) - - _.each( - ["1", "2", ""], - (groupIdStr) => - expectedGroupId = groupIdStr - @view.$(".js-group-select").val(groupIdStr) - @view.$(".js-post-title").val("dummy title") - @view.$(".js-post-body textarea").val("dummy body") - @view.$(".forum-new-post-form").submit() - expect($.ajax).toHaveBeenCalled() - $.ajax.calls.reset() - ) - - describe "always cohort inline discussions ", -> - beforeEach -> - @course_settings = new DiscussionCourseSettings({ - "category_map": { - "children": [], - "entries": {} - }, - "allow_anonymous": false, - "allow_anonymous_to_peers": false, - "is_cohorted": true, - "cohorts": [ - {"id": 1, "name": "Cohort1"}, - {"id": 2, "name": "Cohort2"} - ] - }) - @view = new NewPostView( - el: $("#fixture-element"), - collection: @discussion, - course_settings: @course_settings, - mode: "tab" - ) - - it "disables the cohort menu if it is set false", -> - DiscussionSpecHelper.makeModerator() - @view.is_commentable_cohorted = false - checkVisibility(@view, true, true, true) - - it "enables the cohort menu if it is set true", -> - DiscussionSpecHelper.makeModerator() - @view.is_commentable_cohorted = true - checkVisibility(@view, true, false, true) - - it "is not visible to students when set false", -> - @view.is_commentable_cohorted = false - checkVisibility(@view, false, false, true) - - it "is not visible to students when set true", -> - @view.is_commentable_cohorted = true - checkVisibility(@view, false, false, true) - - describe "cancel post resets form ", -> - beforeEach -> - @course_settings = new DiscussionCourseSettings({ - "allow_anonymous_to_peers":true, - "allow_anonymous":true, - "category_map": { - "subcategories": { - "Week 1": { - "subcategories": {}, - "children": [ - "Topic-Level Student-Visible Label" - ], - "entries": { - "Topic-Level Student-Visible Label": { - "sort_key": null, - "is_cohorted": false, - "id": "2b3a858d0c884eb4b272dbbe3f2ffddd" - } - } - } - }, - "children": [ - "General", - "Week 1" - ], - "entries": { - "General": { - "sort_key": "General", - "is_cohorted": false, - "id": "i4x-waqastest-waqastest-course-waqastest" - } - } - } - }) - - checkPostCancelReset = (mode, discussion, course_settings) -> - view = new NewPostView( - el: $("#fixture-element"), - collection: discussion, - course_settings: course_settings, - mode: mode - ) - view.render() - eventSpy = jasmine.createSpy('eventSpy') - view.listenTo(view, "newPost:cancel", eventSpy) - view.$(".post-errors").html("
  • Title can't be empty
  • ") - view.$("label[for$='post-type-question']").click() - view.$(".js-post-title").val("Test Title") - view.$(".js-post-body textarea").val("Test body") - view.$(".wmd-preview p").html("Test body") - view.$(".js-follow").prop("checked", false) - view.$(".js-anon").prop("checked", true) - view.$(".js-anon-peers").prop("checked", true) - if mode == "tab" - view.$("a[data-discussion-id='2b3a858d0c884eb4b272dbbe3f2ffddd']").click() - view.$(".cancel").click() - expect(eventSpy).toHaveBeenCalled() - expect(view.$(".post-errors").html()).toEqual(""); - expect($("input[id$='post-type-discussion']")).toBeChecked() - expect($("input[id$='post-type-question']")).not.toBeChecked() - expect(view.$(".js-post-title").val()).toEqual(""); - expect(view.$(".js-post-body textarea").val()).toEqual(""); - expect(view.$(".js-follow")).toBeChecked() - expect(view.$(".js-anon")).not.toBeChecked() - expect(view.$(".js-anon-peers")).not.toBeChecked() - if mode == "tab" - expect(view.$(".js-selected-topic").text()).toEqual("General") - - _.each(["tab", "inline"], (mode) => - it "resets the form in #{mode} mode", -> - checkPostCancelReset(mode, @discussion, @course_settings) - ) - - it "posts to the correct URL", -> - topicId = "test_topic" - spyOn($, "ajax").and.callFake( - (params) -> - expect(params.url.path()).toEqual(DiscussionUtil.urlFor("create_thread", topicId)) - {always: ->} - ) - view = new NewPostView( - el: $("#fixture-element"), - collection: @discussion, - course_settings: new DiscussionCourseSettings({ - allow_anonymous: false, - allow_anonymous_to_peers: false - }), - mode: "inline", - topicId: topicId - ) - view.render() - view.$(".forum-new-post-form").submit() - expect($.ajax).toHaveBeenCalled() diff --git a/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee deleted file mode 100644 index a79a4e29cf..0000000000 --- a/common/static/coffee/spec/discussion/view/response_comment_show_view_spec.coffee +++ /dev/null @@ -1,102 +0,0 @@ -describe 'ResponseCommentShowView', -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - # set up the container for the response to go in - DiscussionSpecHelper.setUnderscoreFixtures() - - # set up a model for a new Comment - @comment = new Comment { - id: '01234567', - user_id: '567', - course_id: 'edX/999/test', - body: 'this is a response', - created_at: '2013-04-03T20:08:39Z', - abuse_flaggers: ['123'] - roles: [] - } - @view = new ResponseCommentShowView({ model: @comment }) - spyOn(@view, "convertMath") - - it 'defines the tag', -> - expect($('#jasmine-fixtures')).toExist - expect(@view.tagName).toBeDefined - expect(@view.el.tagName.toLowerCase()).toBe 'li' - - it 'is tied to the model', -> - expect(@view.model).toBeDefined() - - describe 'rendering', -> - - beforeEach -> - spyOn(@view, 'renderAttrs') - - it 'can be flagged for abuse', -> - @comment.flagAbuse() - expect(@comment.get 'abuse_flaggers').toEqual ['123', '567'] - - it 'can be unflagged for abuse', -> - temp_array = [] - temp_array.push(window.user.get('id')) - @comment.set("abuse_flaggers",temp_array) - @comment.unflagAbuse() - expect(@comment.get 'abuse_flaggers').toEqual [] - - describe '_delete', -> - - it 'triggers on the correct events', -> - DiscussionUtil.loadRoles [] - @comment.updateInfo {ability: {'can_delete': true}} - @view.render() - DiscussionViewSpecHelper.checkButtonEvents(@view, "_delete", ".action-delete") - - it 'triggers the delete event', -> - triggerTarget = jasmine.createSpy() - @view.bind "comment:_delete", triggerTarget - @view._delete() - expect(triggerTarget).toHaveBeenCalled() - - describe 'edit', -> - - it 'triggers on the correct events', -> - DiscussionUtil.loadRoles [] - @comment.updateInfo {ability: {'can_edit': true}} - @view.render() - DiscussionViewSpecHelper.checkButtonEvents(@view, "edit", ".action-edit") - - it 'triggers comment:edit when the edit button is clicked', -> - triggerTarget = jasmine.createSpy() - @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') diff --git a/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee b/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee deleted file mode 100644 index c25269ff4e..0000000000 --- a/common/static/coffee/spec/discussion/view/response_comment_view_spec.coffee +++ /dev/null @@ -1,155 +0,0 @@ -describe 'ResponseCommentView', -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - @comment = new Comment { - id: '01234567', - user_id: user.id, - course_id: $$course_id, - body: 'this is a response', - created_at: '2013-04-03T20:08:39Z', - abuse_flaggers: ['123'] - roles: ['Student'] - } - DiscussionSpecHelper.setUnderscoreFixtures() - - @view = new ResponseCommentView({ model: @comment, el: $("#fixture-element") }) - spyOn(ResponseCommentShowView.prototype, "convertMath") - spyOn(DiscussionUtil, "makeWmdEditor") - @view.render() - - describe '_delete', -> - beforeEach -> - @comment.updateInfo {ability: {can_delete: true}} - @event = DiscussionSpecHelper.makeEventSpy() - spyOn(@comment, "remove") - spyOn(@view.$el, "remove") - - setAjaxResult = (isSuccess) -> - spyOn($, "ajax").and.callFake( - (params) => - (if isSuccess then params.success else params.error) {} - {always: ->} - ) - - it 'requires confirmation before deleting', -> - spyOn(window, "confirm").and.returnValue(false) - setAjaxResult(true) - @view._delete(@event) - expect(window.confirm).toHaveBeenCalled() - expect($.ajax).not.toHaveBeenCalled() - expect(@comment.remove).not.toHaveBeenCalled() - - it 'removes the deleted comment object', -> - setAjaxResult(true) - @view._delete(@event) - expect(@comment.remove).toHaveBeenCalled() - expect(@view.$el.remove).toHaveBeenCalled() - - it 'calls the ajax comment deletion endpoint', -> - setAjaxResult(true) - @view._delete(@event) - expect(@event.preventDefault).toHaveBeenCalled() - expect($.ajax).toHaveBeenCalled() - expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/delete') - - it 'handles ajax errors', -> - spyOn(DiscussionUtil, "discussionAlert") - setAjaxResult(false) - @view._delete(@event) - expect(@event.preventDefault).toHaveBeenCalled() - expect($.ajax).toHaveBeenCalled() - expect(@comment.remove).not.toHaveBeenCalled() - expect(@view.$el.remove).not.toHaveBeenCalled() - expect(DiscussionUtil.discussionAlert).toHaveBeenCalled() - - it 'does not delete a comment if the permission is false', -> - @comment.updateInfo {ability: {'can_delete': false}} - spyOn(window, "confirm") - setAjaxResult(true) - @view._delete(@event) - expect(window.confirm).not.toHaveBeenCalled() - expect($.ajax).not.toHaveBeenCalled() - expect(@comment.remove).not.toHaveBeenCalled() - expect(@view.$el.remove).not.toHaveBeenCalled() - - describe 'renderShowView', -> - it 'renders the show view, removes the edit view, and registers event handlers', -> - spyOn(@view, "_delete") - spyOn(@view, "edit") - # Without calling renderEditView first, renderShowView is a no-op - @view.renderEditView() - @view.renderShowView() - @view.showView.trigger "comment:_delete", DiscussionSpecHelper.makeEventSpy() - expect(@view._delete).toHaveBeenCalled() - @view.showView.trigger "comment:edit", DiscussionSpecHelper.makeEventSpy() - expect(@view.edit).toHaveBeenCalled() - 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', -> - spyOn(@view, "update") - spyOn(@view, "cancelEdit") - @view.renderEditView() - @view.editView.trigger "comment:update", DiscussionSpecHelper.makeEventSpy() - expect(@view.update).toHaveBeenCalled() - @view.editView.trigger "comment:cancel_edit", DiscussionSpecHelper.makeEventSpy() - expect(@view.cancelEdit).toHaveBeenCalled() - 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', -> - spyOn(@view, 'renderEditView') - editTarget = jasmine.createSpy() - @view.bind "comment:edit", editTarget - @view.edit() - expect(@view.renderEditView).toHaveBeenCalled() - expect(editTarget).toHaveBeenCalled() - - describe 'with edit view displayed', -> - beforeEach -> - @view.renderEditView() - - describe 'cancelEdit', -> - it 'triggers the appropriate event and switches to the show view', -> - spyOn(@view, 'renderShowView') - cancelEditTarget = jasmine.createSpy() - @view.bind "comment:cancel_edit", cancelEditTarget - @view.cancelEdit() - expect(@view.renderShowView).toHaveBeenCalled() - expect(cancelEditTarget).toHaveBeenCalled() - - describe 'update', -> - beforeEach -> - @updatedBody = "updated body" - # Markdown code creates the editor, so we simulate that here - @view.$el.find(".edit-comment-body").html($("")) - @view.$el.find(".edit-comment-body textarea").val(@updatedBody) - spyOn(@view, 'cancelEdit') - spyOn($, "ajax").and.callFake( - (params) => - if @ajaxSucceed - params.success() - else - params.error({status: 500}) - {always: ->} - ) - - it 'calls the update endpoint correctly and displays the show view on success', -> - @ajaxSucceed = true - @view.update(DiscussionSpecHelper.makeEventSpy()) - expect($.ajax).toHaveBeenCalled() - expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') - expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(@updatedBody) - expect(@view.model.get("body")).toEqual(@updatedBody) - expect(@view.cancelEdit).toHaveBeenCalled() - - it 'handles AJAX errors', -> - originalBody = @comment.get("body") - @ajaxSucceed = false - @view.update(DiscussionSpecHelper.makeEventSpy()) - expect($.ajax).toHaveBeenCalled() - expect($.ajax.calls.mostRecent().args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') - expect($.ajax.calls.mostRecent().args[0].data.body).toEqual(@updatedBody) - expect(@view.model.get("body")).toEqual(originalBody) - expect(@view.cancelEdit).not.toHaveBeenCalled() - expect(@view.$(".edit-comment-form-errors *").length).toEqual(1) diff --git a/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee deleted file mode 100644 index bdedff4789..0000000000 --- a/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee +++ /dev/null @@ -1,230 +0,0 @@ -describe "ThreadResponseShowView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - 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}, - type: "comment" - } - @comment = new Comment(@commentData) - @comment.set("thread", @thread) - @view = new ThreadResponseShowView({ model: @comment, $el: $("#fixture-element") }) - - # Avoid unnecessary boilerplate - spyOn(ThreadResponseShowView.prototype, "convertMath") - - @view.render() - - describe "voting", -> - - it "renders the vote state correctly", -> - DiscussionViewSpecHelper.checkRenderVote(@view, @comment) - - it "check the vote classes after renders", -> - DiscussionViewSpecHelper.checkVoteClasses(@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", - "user_id": "test_id", - "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 - ) - expect(@view.$(".posted-details > a").attr('href')).toEqual("/courses/edX/999/test/discussion/forum/users/test_id") - - 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", - "user_id": "test_id", - "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 - ) - expect(@view.$(".posted-details > a").attr('href')).toEqual("/courses/edX/999/test/discussion/forum/users/test_id") - - 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", -> - spyOn($, "ajax").and.returnValue($.Deferred()) - 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", -> - spyOn($, "ajax").and.returnValue($.Deferred()) - 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", -> - spyOn($, "ajax").and.returnValue($.Deferred()) - @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').and.returnValue('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').and.returnValue(true) - $el = $('#fixture-element').html(@view.getEndorserDisplay()) - checkUserLink($el, true, false) - - it "renders correctly for a staff-endorsed response", -> - spyOn(DiscussionUtil, 'isStaff').and.returnValue(true) - $el = $('#fixture-element').html(@view.getEndorserDisplay()) - checkUserLink($el, false, true) diff --git a/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee b/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee deleted file mode 100644 index 2b4f125a2a..0000000000 --- a/common/static/coffee/spec/discussion/view/thread_response_view_spec.coffee +++ /dev/null @@ -1,92 +0,0 @@ -describe 'ThreadResponseView', -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - - @thread = new Thread({"thread_type": "discussion"}) - @response = new Comment { - children: [{}, {}], - thread: @thread, - } - @view = new ThreadResponseView({model: @response, el: $("#fixture-element")}) - spyOn(ThreadResponseShowView.prototype, "render") - spyOn(ResponseCommentView.prototype, "render") - - describe 'closed and open Threads', -> - checkCommentForm = (closed) -> - thread = new Thread({"thread_type": "discussion", "closed": closed}) - commentData = { - id: "dummy", - user_id: "567", - course_id: "TestOrg/TestCourse/TestRun", - body: "this is a comment", - created_at: "2013-04-03T20:08:39Z", - abuse_flaggers: [], - type: "comment", - children: [], - thread: thread, - } - comment = new Comment(commentData) - view = new ThreadResponseView({ - model: comment, el: $("#fixture-element"), - }) - view.render() - expect(view.$('.comment-form').closest('li').is(":visible")).toBe(not closed) - - it 'hides comment form when thread is closed', -> - checkCommentForm(true) - - it 'show comment form when thread is open', -> - checkCommentForm(false) - - 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: [], thread: @thread } - @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() - spyOn(@view, 'cancelEdit') - spyOn(@view, 'cancelCommentEdits') - spyOn(@view, 'hideCommentForm') - spyOn(@view, 'showCommentForm') - @view.renderComments() - expect(@view.commentViews.length).toEqual(2) - @view.commentViews[0].trigger "comment:edit", jasmine.createSpyObj("event", ["preventDefault"]) - expect(@view.cancelEdit).toHaveBeenCalled() - expect(@view.cancelCommentEdits).toHaveBeenCalled() - expect(@view.hideCommentForm).toHaveBeenCalled() - @view.commentViews[0].trigger "comment:cancel_edit" - expect(@view.showCommentForm).toHaveBeenCalled() - - describe 'cancelCommentEdits', -> - it 'calls cancelEdit on each comment view', -> - @view.renderComments() - expect(@view.commentViews.length).toEqual(2) - _.each(@view.commentViews, (commentView) -> spyOn(commentView, 'cancelEdit')) - @view.cancelCommentEdits() - _.each(@view.commentViews, (commentView) -> expect(commentView.cancelEdit).toHaveBeenCalled()) diff --git a/common/static/coffee/src/discussion/.gitignore b/common/static/coffee/src/discussion/.gitignore deleted file mode 100644 index 3c396b9365..0000000000 --- a/common/static/coffee/src/discussion/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -!views/discussion_thread_edit_view.js -!views/discussion_topic_menu_view.js diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee deleted file mode 100644 index 49f199ab1c..0000000000 --- a/common/static/coffee/src/discussion/content.coffee +++ /dev/null @@ -1,218 +0,0 @@ -if Backbone? - class @Content extends Backbone.Model - - @contents: {} - @contentInfos: {} - - template: -> DiscussionUtil.getTemplate('_content') - - actions: - editable: '.admin-edit' - can_reply: '.discussion-reply' - can_delete: '.admin-delete' - can_openclose: '.admin-openclose' - can_report: '.admin-report' - can_vote: '.admin-vote' - - urlMappers: {} - - urlFor: (name) -> - @urlMappers[name].apply(@) - - can: (action) -> - (@get('ability') || {})[action] - - # Default implementation - canBeEndorsed: -> false - - updateInfo: (info) -> - if info - @set('ability', info.ability) - @set('voted', info.voted) - @set('subscribed', info.subscribed) - - addComment: (comment, options) -> - options ||= {} - if not options.silent - thread = @get('thread') - comments_count = parseInt(thread.get('comments_count')) - thread.set('comments_count', comments_count + 1) - @get('children').push comment - model = new Comment $.extend {}, comment, { thread: @get('thread') } - @get('comments').add model - @trigger "comment:add" - model - - removeComment: (comment) -> - thread = @get('thread') - comments_count = parseInt(thread.get('comments_count')) - thread.set('comments_count', comments_count - 1 - comment.getCommentsCount()) - @trigger "comment:remove" - - resetComments: (children) -> - @set 'children', [] - @set 'comments', new Comments() - for comment in (children || []) - @addComment comment, { silent: true } - - initialize: -> - Content.addContent @id, @ - userId = @get('user_id') - if userId? - @set('staff_authored', DiscussionUtil.isStaff(userId)) - @set('community_ta_authored', DiscussionUtil.isTA(userId)) - else - @set('staff_authored', false) - @set('community_ta_authored', false) - if Content.getInfo(@id) - @updateInfo(Content.getInfo(@id)) - @set 'user_url', DiscussionUtil.urlFor('user_profile', userId) - @resetComments(@get('children')) - - remove: -> - - if @get('type') == 'comment' - @get('thread').removeComment(@) - @get('thread').trigger "comment:remove", @ - else - @trigger "thread:remove", @ - - @addContent: (id, content) -> @contents[id] = content - - @getContent: (id) -> @contents[id] - - @getInfo: (id) -> - @contentInfos[id] - - @loadContentInfos: (infos) -> - for id, info of infos - if @getContent(id) - @getContent(id).updateInfo(info) - $.extend @contentInfos, infos - - pinThread: -> - pinned = @get("pinned") - @set("pinned",pinned) - @trigger "change", @ - - unPinThread: -> - pinned = @get("pinned") - @set("pinned",pinned) - @trigger "change", @ - - flagAbuse: -> - temp_array = @get("abuse_flaggers") - temp_array.push(window.user.get('id')) - @set("abuse_flaggers",temp_array) - @trigger "change", @ - - unflagAbuse: -> - @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: -> - @incrementVote(1) - - unvote: -> - @incrementVote(-1) - - class @Thread extends @Content - urlMappers: - 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @.get('commentable_id'), @id) - 'reply' : -> DiscussionUtil.urlFor('create_comment', @id) - 'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id) - 'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id) - 'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id) - 'close' : -> DiscussionUtil.urlFor('openclose_thread', @id) - 'update' : -> DiscussionUtil.urlFor('update_thread', @id) - '_delete' : -> DiscussionUtil.urlFor('delete_thread', @id) - 'follow' : -> DiscussionUtil.urlFor('follow_thread', @id) - 'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id) - 'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id) - 'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id) - 'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id) - 'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id) - - initialize: -> - @set('thread', @) - super() - - comment: -> - @set("comments_count", parseInt(@get("comments_count")) + 1) - - follow: -> - @set('subscribed', true) - - unfollow: -> - @set('subscribed', false) - - display_body: -> - if @has("highlighted_body") - String(@get("highlighted_body")).replace(//g, '').replace(/<\/highlight>/g, '') - else - @get("body") - - display_title: -> - if @has("highlighted_title") - String(@get("highlighted_title")).replace(//g, '').replace(/<\/highlight>/g, '') - else - @get("title") - - toJSON: -> - json_attributes = _.clone(@attributes) - _.extend(json_attributes, { title: @display_title(), body: @display_body() }) - - created_at_date: -> - new Date(@get("created_at")) - - created_at_time: -> - new Date(@get("created_at")).getTime() - - hasResponses: -> - @get('comments_count') > 0 - - class @Comment extends @Content - urlMappers: - 'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id) - 'unvote': -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id) - 'upvote': -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id) - 'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id) - 'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id) - 'update': -> DiscussionUtil.urlFor('update_comment', @id) - '_delete': -> DiscussionUtil.urlFor('delete_comment', @id) - 'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id) - 'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id) - - getCommentsCount: -> - count = 0 - @get('comments').each (comment) -> - 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 - - initialize: -> - @bind "add", (item) => - item.collection = @ - - find: (id) -> - _.first @where(id: id) diff --git a/common/static/coffee/src/discussion/discussion.coffee b/common/static/coffee/src/discussion/discussion.coffee deleted file mode 100644 index 3b0c68b1ef..0000000000 --- a/common/static/coffee/src/discussion/discussion.coffee +++ /dev/null @@ -1,131 +0,0 @@ -if Backbone? - class @Discussion extends Backbone.Collection - model: Thread - - initialize: (models, options={})-> - @pages = options['pages'] || 1 - @current_page = 1 - @sort_preference = options['sort'] - @bind "add", (item) => - item.discussion = @ - @setSortComparator(@sort_preference) - @on "thread:remove", (thread) => - @remove(thread) - - find: (id) -> - _.first @where(id: id) - - hasMorePages: -> - @current_page < @pages - - setSortComparator: (sortBy) -> - switch sortBy - when 'activity' then @comparator = @sortByDateRecentFirst - when 'votes' then @comparator = @sortByVotes - when 'comments' then @comparator = @sortByComments - - addThread: (thread, options) -> - # TODO: Check for existing thread with same ID in a faster way - if not @find(thread.id) - options ||= {} - model = new Thread thread - @add model - model - - 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' - data['text'] = options.search_text - when 'commentables' - url = DiscussionUtil.urlFor 'search' - data['commentable_ids'] = options.commentable_ids - when 'all' - url = DiscussionUtil.urlFor 'threads' - when 'followed' - url = DiscussionUtil.urlFor 'followed_threads', options.user_id - if options['group_id'] - data['group_id'] = options['group_id'] - data['sort_key'] = sort_options.sort_key || 'activity' - data['sort_order'] = sort_options.sort_order || 'desc' - DiscussionUtil.safeAjax - $elem: @$el - url: url - data: data - dataType: 'json' - success: (response, textStatus) => - models = @models - new_threads = [new Thread(data) for data in response.discussion_data][0] - new_collection = _.union(models, new_threads) - Content.loadContentInfos(response.annotated_content_info) - @pages = response.num_pages - @current_page = response.page - @reset new_collection - error: error - - sortByDate: (thread) -> - # - # The comment client asks each thread for a value by which to sort the collection - # and calls this sort routine regardless of the order returned from the LMS/comments service - # so, this takes advantage of this per-thread value and returns tomorrow's date - # for pinned threads, ensuring that they appear first, (which is the intent of pinned threads) - # - @pinnedThreadsSortComparatorWithDate(thread, true) - - - sortByDateRecentFirst: (thread) -> - # - # Same as above - # but negative to flip the order (newest first) - # - @pinnedThreadsSortComparatorWithDate(thread, false) - #return String.fromCharCode.apply(String, - # _.map(thread.get("created_at").split(""), - # ((c) -> return 0xffff - c.charChodeAt())) - #) - - sortByVotes: (thread1, thread2) -> - thread1_count = parseInt(thread1.get("votes")['up_count']) - thread2_count = parseInt(thread2.get("votes")['up_count']) - @pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count) - - sortByComments: (thread1, thread2) -> - thread1_count = parseInt(thread1.get("comments_count")) - thread2_count = parseInt(thread2.get("comments_count")) - @pinnedThreadsSortComparatorWithCount(thread1, thread2, thread1_count, thread2_count) - - pinnedThreadsSortComparatorWithCount: (thread1, thread2, thread1_count, thread2_count) -> - # if threads are pinned they should be displayed on top. - # Unpinned will be sorted by their property count - if thread1.get('pinned') and not thread2.get('pinned') - -1 - else if thread2.get('pinned') and not thread1.get('pinned') - 1 - else - if thread1_count > thread2_count - -1 - else if thread2_count > thread1_count - 1 - else - if thread1.created_at_time() > thread2.created_at_time() - -1 - else - 1 - - pinnedThreadsSortComparatorWithDate: (thread, ascending)-> - # if threads are pinned they should be displayed on top. - # Unpinned will be sorted by their last activity date - threadLastActivityAtTime = new Date(thread.get("last_activity_at")).getTime() - if thread.get('pinned') - #use tomorrow's date - today = new Date(); - preferredDate = new Date(today.getTime() + (24 * 60 * 60 * 1000) + threadLastActivityAtTime); - else - preferredDate = threadLastActivityAtTime - if ascending - preferredDate - else - -(preferredDate) diff --git a/common/static/coffee/src/discussion/discussion_module_view.coffee b/common/static/coffee/src/discussion/discussion_module_view.coffee deleted file mode 100644 index 198d48d34c..0000000000 --- a/common/static/coffee/src/discussion/discussion_module_view.coffee +++ /dev/null @@ -1,173 +0,0 @@ -if Backbone? - class @DiscussionModuleView extends Backbone.View - events: - "click .discussion-show": "toggleDiscussion" - "keydown .discussion-show": - (event) -> DiscussionUtil.activateOnSpace(event, @toggleDiscussion) - "click .new-post-btn": "toggleNewPost" - "keydown .new-post-btn": - (event) -> DiscussionUtil.activateOnSpace(event, @toggleNewPost) - "click .discussion-paginator a": "navigateToPage" - - page_re: /\?discussion_page=(\d+)/ - initialize: (options) -> - @toggleDiscussionBtn = @$(".discussion-show") - # Set the page if it was set in the URL. This is used to allow deep linking to pages - match = @page_re.exec(window.location.href) - @context = options.context or "course" # allowed values are "course" or "standalone" - if match - @page = parseInt(match[1]) - else - @page = 1 - - toggleNewPost: (event) => - event.preventDefault() - if !@newPostForm - @toggleDiscussion() - @isWaitingOnNewPost = true; - return - if @showed - @newPostForm.slideDown(300) - else - @newPostForm.show().focus() - @toggleDiscussionBtn.addClass('shown') - @toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion")) - @$("section.discussion").slideDown() - @showed = true - - hideNewPost: => - @newPostForm.slideUp(300) - - hideDiscussion: => - @$("section.discussion").slideUp() - @toggleDiscussionBtn.removeClass('shown') - @toggleDiscussionBtn.find('.button-text').html(gettext("Show Discussion")) - @showed = false - - toggleDiscussion: (event) => - if @showed - @hideDiscussion() - else - @toggleDiscussionBtn.addClass('shown') - @toggleDiscussionBtn.find('.button-text').html(gettext("Hide Discussion")) - - if @retrieved - @$("section.discussion").slideDown() - @showed = true - else - $elem = @toggleDiscussionBtn - @loadPage( - $elem, - => - @hideDiscussion() - DiscussionUtil.discussionAlert( - gettext("Sorry"), - gettext("We had some trouble loading the discussion. Please try again.") - ) - ) - - loadPage: ($elem, error) => - discussionId = @$el.data("discussion-id") - url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}" - DiscussionUtil.safeAjax - $elem: $elem - $loading: $elem - takeFocus: true - url: url - type: "GET" - dataType: 'json' - success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId) - error: error - - renderDiscussion: ($elem, response, textStatus, discussionId) => - $elem.focus() - user = new DiscussionUser(response.user_info) - window.user = user - DiscussionUtil.setUser(user) - Content.loadContentInfos(response.annotated_content_info) - DiscussionUtil.loadRoles(response.roles) - - @course_settings = new DiscussionCourseSettings(response.course_settings) - @discussion = new Discussion() - @discussion.reset(response.discussion_data, {silent: false}) - - $discussion = _.template($("#inline-discussion-template").html())( - 'threads': response.discussion_data, - 'discussionId': discussionId - ) - if @$('section.discussion').length - @$('section.discussion').replaceWith($discussion) - else - @$el.append($discussion) - - @newPostForm = this.$el.find('.new-post-article') - @threadviews = @discussion.map (thread) => - view = new DiscussionThreadView( - el: @$("article#thread_#{thread.id}"), - model: thread, - mode: "inline", - context: @context, - course_settings: @course_settings, - topicId: discussionId - ) - thread.on "thread:thread_type_updated", -> - view.rerender() - view.expand() - return view - _.each @threadviews, (dtv) -> dtv.render() - DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) - @newPostView = new NewPostView( - el: @newPostForm, - collection: @discussion, - course_settings: @course_settings, - topicId: discussionId, - is_commentable_cohorted: response.is_commentable_cohorted - ) - @newPostView.render() - @listenTo( @newPostView, 'newPost:cancel', @hideNewPost ) - @discussion.on "add", @addThread - - @retrieved = true - @showed = true - @renderPagination(response.num_pages) - - if @isWaitingOnNewPost - @newPostForm.show().focus() - - addThread: (thread, collection, options) => - # TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1? - article = $("
    ") - @$('section.discussion > .threads').prepend(article) - - threadView = new DiscussionThreadView( - el: article, - model: thread, - mode: "inline", - context: @context, - course_settings: @course_settings, - topicId: @$el.data("discussion-id") - ) - threadView.render() - @threadviews.unshift threadView - - renderPagination: (numPages) => - pageUrl = (number) -> - "?discussion_page=#{number}" - params = DiscussionUtil.getPaginationParams(@page, numPages, pageUrl) - pagination = _.template($("#pagination-template").html())(params) - @$('section.discussion-pagination').html(pagination) - - navigateToPage: (event) => - event.preventDefault() - window.history.pushState({}, window.document.title, event.target.href) - currPage = @page - @page = $(event.target).data('page-number') - @loadPage( - $(event.target), - => - @page = currPage - DiscussionUtil.discussionAlert( - gettext("Sorry"), - gettext("We had some trouble loading the threads you requested. Please try again.") - ) - ) diff --git a/common/static/coffee/src/discussion/discussion_router.coffee b/common/static/coffee/src/discussion/discussion_router.coffee deleted file mode 100644 index 5b90925b67..0000000000 --- a/common/static/coffee/src/discussion/discussion_router.coffee +++ /dev/null @@ -1,90 +0,0 @@ -if Backbone? - class @DiscussionRouter extends Backbone.Router - routes: - "": "allThreads" - ":forum_name/threads/:thread_id" : "showThread" - - initialize: (options) -> - @discussion = options['discussion'] - @course_settings = options['course_settings'] - - @nav = new DiscussionThreadListView( - collection: @discussion, - el: $(".forum-nav"), - courseSettings: @course_settings - ) - @nav.on "thread:selected", @navigateToThread - @nav.on "thread:removed", @navigateToAllThreads - @nav.on "threads:rendered", @setActiveThread - @nav.on "thread:created", @navigateToThread - @nav.render() - - @newPost = $('.new-post-article') - @newPostView = new NewPostView( - el: @newPost, - collection: @discussion, - course_settings: @course_settings, - mode: "tab" - ) - @newPostView.render() - @listenTo( @newPostView, 'newPost:cancel', @hideNewPost ) - $('.new-post-btn').bind "click", @showNewPost - $('.new-post-btn').bind "keydown", (event) => DiscussionUtil.activateOnSpace(event, @showNewPost) - - allThreads: -> - @nav.updateSidebar() - @nav.goHome() - - setActiveThread: => - if @thread - @nav.setActiveThread(@thread.get("id")) - else - @nav.goHome - - showThread: (forum_name, thread_id) -> - @thread = @discussion.get(thread_id) - @thread.set("unread_comments_count", 0) - @thread.set("read", true) - @setActiveThread() - @showMain() - - showMain: => - if(@main) - @main.cleanup() - @main.undelegateEvents() - unless($(".forum-content").is(":visible")) - $(".forum-content").fadeIn() - if(@newPost.is(":visible")) - @newPost.fadeOut() - - @main = new DiscussionThreadView( - el: $(".forum-content"), - model: @thread, - mode: "tab", - course_settings: @course_settings, - ) - @main.render() - @main.on "thread:responses:rendered", => - @nav.updateSidebar() - @thread.on "thread:thread_type_updated", @showMain - - navigateToThread: (thread_id) => - thread = @discussion.get(thread_id) - @navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true) - - navigateToAllThreads: => - @navigate("", trigger: true) - - showNewPost: (event) => - $('.forum-content').fadeOut( - duration: 200 - complete: => - @newPost.fadeIn(200).focus() - ) - - hideNewPost: => - @newPost.fadeOut( - duration: 200 - complete: => - $('.forum-content').fadeIn(200).find('.thread-wrapper').focus() - ) diff --git a/common/static/coffee/src/discussion/main.coffee b/common/static/coffee/src/discussion/main.coffee deleted file mode 100644 index 5c663ffa21..0000000000 --- a/common/static/coffee/src/discussion/main.coffee +++ /dev/null @@ -1,38 +0,0 @@ -if Backbone? - DiscussionApp = - start: (elem)-> - # TODO: Perhaps eliminate usage of global variables when possible - DiscussionUtil.loadRolesFromContainer() - element = $(elem) - window.$$course_id = element.data("course-id") - window.courseName = element.data("course-name") - user_info = element.data("user-info") - sort_preference = element.data("sort-preference") - threads = element.data("threads") - thread_pages = element.data("thread-pages") - content_info = element.data("content-info") - user = new DiscussionUser(user_info) - DiscussionUtil.setUser(user) - window.user = user - Content.loadContentInfos(content_info) - discussion = new Discussion(threads, {pages: thread_pages, sort: sort_preference}) - course_settings = new DiscussionCourseSettings(element.data("course-settings")) - new DiscussionRouter({discussion: discussion, course_settings: course_settings}) - Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"}) - DiscussionProfileApp = - start: (elem) -> - # Roles are not included in user profile page, but they are not used for anything - DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []}) - element = $(elem) - window.$$course_id = element.data("course-id") - threads = element.data("threads") - user_info = element.data("user-info") - window.user = new DiscussionUser(user_info) - page = element.data("page") - numPages = element.data("num-pages") - new DiscussionUserProfileView(el: element, collection: threads, page: page, numPages: numPages) - $ -> - $("section.discussion").each (index, elem) -> - DiscussionApp.start(elem) - $("section.discussion-user-threads").each (index, elem) -> - DiscussionProfileApp.start(elem) diff --git a/common/static/coffee/src/discussion/models/discussion_course_settings.coffee b/common/static/coffee/src/discussion/models/discussion_course_settings.coffee deleted file mode 100644 index 97275a639b..0000000000 --- a/common/static/coffee/src/discussion/models/discussion_course_settings.coffee +++ /dev/null @@ -1,2 +0,0 @@ -if Backbone? - class @DiscussionCourseSettings extends Backbone.Model diff --git a/common/static/coffee/src/discussion/models/discussion_user.coffee b/common/static/coffee/src/discussion/models/discussion_user.coffee deleted file mode 100644 index 892727c523..0000000000 --- a/common/static/coffee/src/discussion/models/discussion_user.coffee +++ /dev/null @@ -1,15 +0,0 @@ -if Backbone? - class @DiscussionUser extends Backbone.Model - following: (thread) -> - _.include(@get('subscribed_thread_ids'), thread.id) - - voted: (thread) -> - _.include(@get('upvoted_ids'), thread.id) - - vote: (thread) -> - @get('upvoted_ids').push(thread.id) - thread.vote() - - unvote: (thread) -> - @set('upvoted_ids', _.without(@get('upvoted_ids'), thread.id)) - thread.unvote() diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee deleted file mode 100644 index 06f1667e13..0000000000 --- a/common/static/coffee/src/discussion/utils.coffee +++ /dev/null @@ -1,346 +0,0 @@ -class @DiscussionUtil - - @wmdEditors: {} - - @getTemplate: (id) -> - $("script##{id}").html() - - @setUser: (user) -> - @user = user - - @getUser: () -> - @user - - @loadRoles: (roles)-> - @roleIds = roles - - @loadRolesFromContainer: -> - @loadRoles($("#discussion-container").data("roles")) - - @isStaff: (user_id) -> - user_id ?= @user?.id - staff = _.union(@roleIds['Moderator'], @roleIds['Administrator']) - _.include(staff, parseInt(user_id)) - - @isTA: (user_id) -> - user_id ?= @user?.id - 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) - - @generateDiscussionLink: (cls, txt, handler) -> - $("").addClass("discussion-link") - .attr("href", "javascript:void(0)") - .addClass(cls).html(txt) - .click -> handler(this) - - @urlFor: (name, param, param1, param2) -> - { - follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow" - unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow" - create_thread : "/courses/#{$$course_id}/discussion/#{param}/threads/create" - update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update" - create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply" - delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete" - flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse" - unFlagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unFlagAbuse" - flagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/flagAbuse" - unFlagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unFlagAbuse" - upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote" - downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote" - pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin" - un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin" - undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote" - follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow" - unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow" - update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update" - endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse" - create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply" - delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete" - upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote" - downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote" - undo_vote_for_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unvote" - upload : "/courses/#{$$course_id}/discussion/upload" - users : "/courses/#{$$course_id}/discussion/users" - search : "/courses/#{$$course_id}/discussion/forum/search" - retrieve_discussion : "/courses/#{$$course_id}/discussion/forum/#{param}/inline" - retrieve_single_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}" - openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close" - permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}" - permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}" - user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}" - followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed" - threads : "/courses/#{$$course_id}/discussion/forum" - "enable_notifications" : "/notification_prefs/enable/" - "disable_notifications" : "/notification_prefs/disable/" - "notifications_status" : "/notification_prefs/status/" - }[name] - - @ignoreEnterKey: (event) => - if event.which == 13 - event.preventDefault() - - @activateOnSpace: (event, func) -> - if event.which == 32 - event.preventDefault() - func(event) - - @makeFocusTrap: (elem) -> - elem.keydown( - (event) -> - if event.which == 9 # Tab - event.preventDefault() - ) - - @showLoadingIndicator: (element, takeFocus) -> - @$_loading = $("
    " + gettext("Loading content") + "
    ") - element.after(@$_loading) - if takeFocus - @makeFocusTrap(@$_loading) - @$_loading.focus() - - @hideLoadingIndicator: () -> - @$_loading.remove() - - @discussionAlert: (header, body) -> - if $("#discussion-alert").length == 0 - alertDiv = $("