discussions: add publish icon to index and show pages

fixes CNVS-9265

test plan
- ensure that the publish icon is not visible when the account
 level draft state variable is unset
- ensure that the publish icon is visible when the account level
 draft state setting is set
- ensure that ungraded topics with no replies can be published/unpublished

Change-Id: Ia93b91aa54812610e5c07e063f512e605d6235f2
Reviewed-on: https://gerrit.instructure.com/23470
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Zach Pendleton <zachp@instructure.com>
QA-Review: Steven Shepherd <sshepherd@instructure.com>
Product-Review: Braden Anderson <banderson@instructure.com>
This commit is contained in:
Joel Hough 2013-07-11 13:51:19 -06:00 committed by Braden Anderson
parent adbd7b8c0b
commit 1941d97d90
16 changed files with 257 additions and 90 deletions

View File

@ -120,11 +120,11 @@ require [
EntryView.collapseRootEntries()
scrollToTop()
toolbarView.on 'markAllAsRead', ->
topicView.on 'markAllAsRead', ->
data.markAllAsRead()
setAllReadStateAllViews('read')
toolbarView.on 'markAllAsUnread', ->
topicView.on 'markAllAsUnread', ->
data.markAllAsUnread()
setAllReadStateAllViews('unread')

View File

@ -20,6 +20,7 @@ define [
subscribed: false
user_can_see_posts: true
subscription_hold: null
publishable: true
dateAttributes: [
'last_reply_at'
@ -41,11 +42,20 @@ define [
assign = new Assignment(assign_attributes)
assign.alreadyScoped = true
@set 'assignment', assign
@set 'publishable', @get('can_unpublish')
# always include assignment in view presentation
present: =>
Backbone.Model::toJSON.call(this)
publish: ->
@updateOneAttribute('published', true)
unpublish: ->
@updateOneAttribute('published', false)
disabledMessage: -> I18n.t 'cannot_unpublish', "Can't unpublish"
topicSubscribe: ->
baseUrl = _.result this, 'url'
@set 'subscribed', true

View File

@ -19,8 +19,6 @@ define [
'change #onlyUnread': 'toggleUnread'
'click #collapseAll': 'collapseAll'
'click #expandAll': 'expandAll'
'click .mark_all_as_read': 'markAllAsRead'
'click .mark_all_as_unread': 'markAllAsUnread'
initialize: ->
@model.on 'change', @clearInputs
@ -61,14 +59,6 @@ define [
@model.set 'collapsed', false
@trigger 'expandAll'
markAllAsRead: (event) ->
event.preventDefault()
@trigger 'markAllAsRead'
markAllAsUnread: (event) ->
event.preventDefault()
@trigger 'markAllAsUnread'
maybeDisableFields: ->
@$disableWhileFiltering.attr 'disabled', @model.hasFilter()

View File

@ -6,13 +6,14 @@ define [
'compiled/models/DiscussionTopic'
'compiled/views/DiscussionTopic/EntriesView'
'compiled/views/DiscussionTopic/EntryView'
'compiled/views/PublishButtonView',
'jst/discussions/_reply_form'
'compiled/discussions/Reply'
'compiled/widget/assignmentRubricDialog'
'compiled/util/wikiSidebarWithMultipleEditors'
'jquery.instructure_misc_helpers' #scrollSidebar
'str/htmlEscape'
], (I18n, $, Backbone, _, DiscussionTopic, EntriesView, EntryView, replyTemplate, Reply, assignmentRubricDialog, htmlEscape) ->
], (I18n, $, Backbone, _, DiscussionTopic, EntriesView, EntryView, PublishButtonView, replyTemplate, Reply, assignmentRubricDialog, htmlEscape) ->
class TopicView extends Backbone.View
@ -27,10 +28,12 @@ define [
'click .rte_switch_views_link': 'toggleEditorMode'
'click .topic-subscribe-button': 'subscribeTopic'
'click .topic-unsubscribe-button': 'unsubscribeTopic'
'click .mark_all_as_read': 'markAllAsRead'
'click .mark_all_as_unread': 'markAllAsUnread'
els:
'.add_root_reply': '$addRootReply'
'#discussion_topic': '$topic'
'.topic .discussion-entry-reply-area': '$replyLink'
'.due_date_wrapper': '$dueDates'
'.reply-textarea:first': '$textarea'
'#discussion-toolbar': '$discussionToolbar'
@ -55,9 +58,9 @@ define [
hideIfFiltering: =>
if @filterModel.hasFilter()
@$topic.addClass 'hidden'
@$replyLink.addClass 'hidden'
else
@$topic.removeClass 'hidden'
@$replyLink.removeClass 'hidden'
afterRender: ->
super
@ -65,6 +68,9 @@ define [
assignmentRubricDialog.initTriggers()
@$el.toggleClass 'side_comment_discussion', !ENV.DISCUSSION.THREADED
@subscriptionStatusChanged()
if $el = @$('#topic_publish_button')
@topic.set(unpublishable: ENV.DISCUSSION.TOPIC.CAN_UNPUBLISH, published: ENV.DISCUSSION.TOPIC.IS_PUBLISHED)
new PublishButtonView(model: @topic, el: $el).render()
filter: @::afterRender
@ -169,4 +175,10 @@ define [
@addReply event
$('html, body').animate scrollTop: target.offset().top - 100
markAllAsRead: (event) ->
event.preventDefault()
@trigger 'markAllAsRead'
markAllAsUnread: (event) ->
event.preventDefault()
@trigger 'markAllAsUnread'

View File

@ -47,10 +47,13 @@ define [
# Public: Topic is able to be pinned/unpinned.
@optionProperty 'pinnable'
@child 'publishIcon', '[data-view=publishIcon]' if ENV.permissions.publish
@child 'toggleableSubscriptionIcon', '[data-view=toggleableSubscriptionIcon]'
initialize: (options) ->
@attachModel()
options.publishIcon = new PublishIconView(model: @model) if ENV.permissions.publish
options.toggleableSubscriptionIcon = new ToggleableSubscriptionIconView(model: @model)
super
@ -132,3 +135,4 @@ define [
# Returns nothing.
attachModel: ->
@model.on('change:hidden', @hide)
@model.on('change:published', @render)

View File

@ -105,6 +105,7 @@ define [
render: ->
@$el.attr 'role', 'button'
@$el.attr 'tabindex', '0'
@$el.html '<i></i><span class="publish-text"></span>'
@cacheEls()

View File

@ -227,7 +227,8 @@ class DiscussionTopicsController < ApplicationController
permissions: {
create: @context.discussion_topics.new.grants_right?(@current_user, session, :create),
moderate: user_can_moderate,
change_settings: user_can_edit_course_settings?
change_settings: user_can_edit_course_settings?,
publish: user_can_moderate && @domain_root_account.enable_draft?
}}
append_sis_data(hash)
@ -351,6 +352,9 @@ class DiscussionTopicsController < ApplicationController
:TOPIC => {
:ID => @topic.id,
:IS_SUBSCRIBED => @topic.subscribed?(@current_user),
:IS_PUBLISHED => @topic.published?,
:CAN_UNPUBLISH => @topic.can_unpublish?,
},
:PERMISSIONS => {
:CAN_REPLY => @locked ? false : !(@topic.for_group_assignment? || @topic.locked?), # Can reply
@ -645,6 +649,8 @@ class DiscussionTopicsController < ApplicationController
@errors[:published] = t(:error_draft_state_announcement, "This topic cannot be set to draft state because it is an announcement.")
elsif @topic.discussion_subentry_count > 0
@errors[:published] = t(:error_draft_state_with_posts, "This topic cannot be set to draft state because it contains posts.")
elsif @topic.assignment_id
@errors[:published] = t(:error_draft_state_graded, "This topic cannot be set to draft state because it is an assignment")
elsif user_can_moderate
discussion_topic_hash[:delayed_post_at] = nil
@topic.workflow_state = 'post_delayed'

View File

@ -456,8 +456,16 @@ class DiscussionTopic < ActiveRecord::Base
end
def published?
if self.assignment
self.assignment.published?
else
workflow_state != 'post_delayed'
end
end
def can_unpublish?
!self.assignment && self.discussion_subentry_count == 0
end
def can_unpublish?
!self.assignment && self.discussion_subentry_count == 0

View File

@ -1,6 +1,6 @@
@import "environment";
$border: #b6babf;
$border: #C1C7CF;
$borderRadius: 3px;
body.with-right-side.with-left-side {
@ -47,10 +47,10 @@ body.with-right-side.with-left-side {
.entry {
background: white;
border: 1px solid $border;
border-top-left-radius: $borderRadius;
border-top-right-radius: $borderRadius;
border-radius: $borderRadius;
padding: $borderRadius 0 0 0;
margin: 16px 0 16px 18px;
box-shadow: 0 1px 0 #DDE0E4;
&.collapsed, .entry {
margin: 16px 0 16px 18px;
@ -113,6 +113,8 @@ body.with-right-side.with-left-side {
position: relative;
margin: 0;
background: white;
border-radius: $borderRadius;
padding: 6px;
.al-trigger {
margin-left: 10px;
margin-right: 0px;
@ -144,7 +146,7 @@ body.with-right-side.with-left-side {
}
.toggle-wrapper {
margin-bottom: 11px;
margin: 0 11px;
text-align: right;
}
@ -259,23 +261,18 @@ body.with-right-side.with-left-side {
border-top: 1px solid $border;
}
.topic {
.discussion-entry-reply-area {
padding: 0 20px;
border-top: 1px solid #d4d5d7;
border-bottom: 1px solid $border;
}
}
.discussion_entry > .discussion-entry-reply-area {
padding: 0 0.8em;
border-top: 1px solid #d4d5d7;
border: 1px solid #d4d5d7;
margin: 10px;
border-radius: 3px;
}
.entry {
& > .bottom-reply-with-box {
.discussion-entry-reply-area {
padding: 0 0.8em;
border-top: 1px solid #d4d5d7;
border-radius: 0 0 3px 3px;
&.reply-box-container {
padding: 15px 0.8em;
@ -289,6 +286,14 @@ body.with-right-side.with-left-side {
}
}
.topic {
padding: 0;
background: transparent;
> .discussion-entry-reply-area {
margin: 16px 10px 0 28px;
}
}
.discussion-reply-form {
margin: 0;
padding-bottom: 1em;
@ -416,7 +421,7 @@ body.with-right-side.with-left-side {
.entry-controls {
overflow: auto;
padding: 0 10px 2px 10px;
padding: 10px;
font-size: 12px;
.new-and-total-badge {
@ -434,7 +439,8 @@ body.with-right-side.with-left-side {
overflow: auto;
.entry_content {
padding: 10px 10px 0 10px;
padding: 10px;
background: white;
}
}
@ -447,7 +453,7 @@ body.with-right-side.with-left-side {
}
#main {
background: $altBG;
background: #E8ECEF;
}
.discussion_subentries {
@ -531,13 +537,8 @@ body.with-right-side.with-left-side {
}
}
.collapsable {
border-bottom: 1px solid white;
}
.collapsable:hover {
cursor: pointer;
border-color: #e4e5e7;
&:before {
display: block;
content: "";
@ -598,8 +599,16 @@ body.with-right-side.with-left-side {
.toolbarView {
background: white;
min-height: 50px;
&#discussion-toolbar {
.headerBar {
background: #e3e7ec;
border-top: solid 1px $border;
}
}
.admin-links {
text-align: left;
float: none;
display: inline;
}
.al-trigger {
margin-left: 4px;
@ -631,6 +640,10 @@ body.with-right-side.with-left-side {
[disabled] {
display: none;
}
.btn-published {
box-shadow: none;
@include buttonBackground($btnSuccessBackground, $btnSuccessBackgroundHighlight);
}
}
.discussion-topic-due-dates {

View File

@ -179,6 +179,22 @@
display: table-cell;
vertical-align: middle;
}
&.discussion-unpublished {
.discussion-type, .discussion-title a {
color: #696969;
}
}
&.discussion-published {
.discussion-type, .discussion-title a {
color: #34832B;
}
}
}
.draggable-handle {
width: 16px;
color: #B4B7BA;
cursor: pointer;
}
.discussion-type {
@ -188,14 +204,25 @@
.discussion-due-date {
width: 150px;
.discussion-date-type {
font-weight: bold;
}
}
.discussion-status {
color: #bbb;
width: 40px;
width: 60px;
.subscription-toggler:focus {
outline: none;
.publish-icon {
margin-right: 10px;
}
.subscription-toggler {
padding: 5px;
&:before {
margin: 0;
}
}
a[class*=icon-] {

View File

@ -33,46 +33,18 @@
jammit_css :tinymce, :discussions, :learning_outcomes
%>
<div id="discussion-toolbar" class="toolbarView">
<div class="headerBar" data-sticky>
<div id="discussion-managebar" class="toolbarView">
<div class="headerBar">
<div class="row-fluid form-inline" style="overflow: visible;">
<div class="span7">
<input
id = "discussion-search"
type = "text"
aria-label = "<%= t("filter_replies", "Filter replies by search term") %>"
placeholder = "<%= t("search_entries_placeholder", "Search entries or author") %>"
size = 50
>
<input
type = "checkbox"
id = "onlyUnread"
><label for="onlyUnread"><%= t('unread', 'Unread') %></label>
&nbsp; &nbsp;
<button id="collapseAll" class="btn disableWhileFiltering" title="<%= t('collapse_replies', 'Collapse replies') %>" aria-label="<%= t('collapse_replies', 'Collapse replies') %>">
<%= image_tag 'discussions/collapse_icon.png', :alt => t('collapse_replies', 'Collapse replies') %>
</button>
<button id="expandAll" class="btn disableWhileFiltering" title="<%= t('expand_replies', 'Expand replies') %>" aria-label="<%= t('expand_replies', 'Expand replies') %>">
<%= image_tag 'discussions/expand_icon.png', :alt => t('expand_replies', 'Expand replies') %>
<div class="pull-right">
<% if @context.draft_state_enabled? %>
<button
id="topic_publish_button"
data-id='<%= @topic.id %>'
class='btn <%= "published" if @topic.published? %> <%= "disabled" if @topic.published? && !@topic.can_unpublish? %>'>
</button>
<% end %>
</div>
<div class="span5 right-align">
<a href='#' class='btn topic-subscribe-button' data-tooltip="bottom"
title="<%= t('topic_subscribe_tooltip', 'You are unsubscribed and will not be notified of new comments') %>"
aria-label="<%= t('topic_subscribe_tooltip', 'You are unsubscribed and will not be notified of new comments') %>">
<i class='icon-discussion-check' aria-hidden="hidden"></i>
<%= t(:topic_subscribe, 'Subscribe') %>
</a>
<a href='#' class='btn topic-unsubscribe-button btn-success' data-tooltip="bottom"
title="<%= t('topic_subscribed_tooltip', 'You are subscribed and will be notified of new comments') %>"
aria-label="<%= t('topic_subscribed_tooltip', 'You are subscribed and will be notified of new comments') %>">
<i class='icon-discussion-check' aria-hidden="hidden"></i>
<%= t(:topic_unsubscribe, 'Subscribed') %>
</a>
<% if @topic.grants_right?(@current_user, nil, :update) %>
<a href="<%= context_url(@topic.context,
:edit_context_discussion_topic_url,
@ -185,8 +157,10 @@
<div class="discussion-fyi">
<% if @topic.is_a? Announcement %>
<%= t 'announcement_locked', 'This announcement will not be visible to users until *%{date}*', :date => datetime_string(@topic.delayed_post_at) %>
<% else %>
<% elsif @topic.delayed_post_at %>
<%= t 'topic_locked', 'This topic will not be visible to users until *%{date}*', :date => datetime_string(@topic.delayed_post_at) %>
<% else %>
<%= t 'topic_not_visible', 'This topic is not visible to users' %>
<% end %>
</div>
<% end %>
@ -227,8 +201,51 @@
</div>
<% end %>
</div>
</div>
<div id="discussion-toolbar" class="toolbarView">
<div class="headerBar" data-sticky>
<div class="row-fluid form-inline" style="overflow: visible;">
<div class="span9">
<input
id = "discussion-search"
type = "text"
aria-label = "<%= t("filter_replies", "Filter replies by search term") %>"
placeholder = "<%= t("search_entries_placeholder", "Search entries or author") %>"
size = 50
>
<input
type = "checkbox"
id = "onlyUnread"
><label for="onlyUnread"><%= t('unread', 'Unread') %></label>
&nbsp; &nbsp;
<button id="collapseAll" class="btn disableWhileFiltering" title="<%= t('collapse_replies', 'Collapse replies') %>" aria-label="<%= t('collapse_replies', 'Collapse replies') %>">
<%= image_tag 'discussions/collapse_icon.png', :alt => t('collapse_replies', 'Collapse replies') %>
</button>
<button id="expandAll" class="btn disableWhileFiltering" title="<%= t('expand_replies', 'Expand replies') %>" aria-label="<%= t('expand_replies', 'Expand replies') %>">
<%= image_tag 'discussions/expand_icon.png', :alt => t('expand_replies', 'Expand replies') %>
</button>
</div>
<div class="span3 right-align">
<a href='#' class='btn topic-subscribe-button' data-tooltip="bottom"
title="<%= t('topic_subscribe_tooltip', 'You are unsubscribed and will not be notified of new comments') %>"
aria-label="<%= t('topic_subscribe_tooltip', 'You are unsubscribed and will not be notified of new comments') %>">
<i class='icon-discussion-check' aria-hidden="hidden"></i>
<%= t(:topic_subscribe, 'Subscribe') %>
</a>
<a href='#' class='btn topic-unsubscribe-button btn-success' data-tooltip="bottom"
title="<%= t('topic_subscribed_tooltip', 'You are subscribed and will be notified of new comments') %>"
aria-label="<%= t('topic_subscribed_tooltip', 'You are subscribed and will be notified of new comments') %>">
<i class='icon-discussion-check' aria-hidden="hidden"></i>
<%= t(:topic_unsubscribe, 'Subscribed') %>
</a>
</div>
</div>
</div>
</div>
</article>

View File

@ -1,5 +1,11 @@
<div class="discussion-content">
<div class="discussion-row">
<div class="discussion-row{{#if ENV.permissions.publish}} {{#if published}}discussion-published{{else}}discussion-unpublished{{/if}}{{/if}}">
{{#if permissions.moderate}}
<div class="draggable-handle">
<i class="icon-drag-handle"></i>
</div>
{{/if}}
<div class="discussion-type">
{{#if assignment}}
<i class="icon-assignment" aria-hidden="true"></i>
@ -15,7 +21,7 @@
<div class="discussion-due-date">
{{#if assignment}}
{{#if assignment.due_at}}
{{#t "due"}}Due{{/t}} {{tDateToString assignment.due_at "medium"}}
<span class="discussion-date-type">{{#t "due"}}Due{{/t}}</span> {{tDateToString assignment.due_at "medium"}}
{{else}}
{{#t "no_due_date"}}No Due Date{{/t}}
{{/if}}
@ -23,6 +29,8 @@
</div>
<div class="discussion-status">
<span id="publish-icon" class="publish-icon" data-view="publishIcon"></span>
<a href="#" data-view="toggleableSubscriptionIcon" class="subscription-toggler">
<span class="screenreader-only">
{{#if subscribed}}
@ -32,8 +40,6 @@
{{/if}}
</span>
</a>
<!-- <i class="icon-unpublished"></i> -->
</div>
<div class="discussion-unread-status">

View File

@ -62,6 +62,7 @@ module Api::V1::DiscussionTopics
:topic_children => children,
:attachments => attachments,
:published => topic.published?,
:can_unpublish => topic.can_unpublish?,
:locked => topic.locked?,
:author => user_display_json(topic.user, topic.context),
:html_url => context.is_a?(CollectionItem) ? nil :

View File

@ -1130,6 +1130,8 @@ describe AssignmentsApiController, :type => :integration do
'unread_count' => 0,
'user_can_see_posts' => @topic.user_can_see_posts?(@user),
'subscribed' => @topic.subscribed?(@user),
'published' => @topic.published?,
'can_unpublish' => @topic.can_unpublish?,
'url' =>
"http://www.example.com/courses/#{@course.id}/discussion_topics/#{@topic.id}",
'html_url' =>

View File

@ -255,6 +255,7 @@ describe DiscussionTopicsController, :type => :integration do
"discussion_subentry_count"=>0,
"assignment_id"=>nil,
"published"=>true,
"can_unpublish"=>true,
"delayed_post_at"=>nil,
"lock_at"=>nil,
"id"=>@topic.id,
@ -817,6 +818,7 @@ describe DiscussionTopicsController, :type => :integration do
"discussion_subentry_count"=>0,
"assignment_id"=>nil,
"published"=>true,
"can_unpublish"=>true,
"delayed_post_at"=>nil,
"lock_at"=>nil,
"id"=>gtopic.id,

View File

@ -182,7 +182,7 @@ describe "discussions" do
it "should validate closing the discussion for comments" do
create_and_go_to_topic
f("#discussion-toolbar .al-trigger").click
f("#discussion-managebar .al-trigger").click
expect_new_page_load { f(".discussion_locked_toggler").click }
f('.discussion-fyi').text.should == 'This topic is closed for comments'
ff('.discussion-reply-action').should be_empty
@ -191,7 +191,7 @@ describe "discussions" do
it "should validate reopening the discussion for comments" do
create_and_go_to_topic('closed discussion', 'side_comment', true)
f("#discussion-toolbar .al-trigger").click
f("#discussion-managebar .al-trigger").click
expect_new_page_load { f(".discussion_locked_toggler").click }
ff('.discussion-reply-action').should_not be_empty
DiscussionTopic.last.workflow_state.should == 'active'
@ -439,6 +439,74 @@ describe "discussions" do
topic.reload
topic.subscribed?(@user).should be_false
end
context "draft state" do
before do
a = Account.default
a.settings[:enable_draft] = true
a.save!
end
def click_publish_icon(topic)
fj(".discussion[data-id=#{topic.id}] .publish-icon i").click
wait_for_ajaximations
end
it "should allow publishing a discussion" do
topic = @course.discussion_topics.create!(title: 'Publish me', user: @user)
topic.workflow_state = 'post_delayed'
topic.save!
topic.published?.should be_false
get(url)
click_publish_icon topic
topic.reload
topic.published?.should be_true
end
it "should allow unpublishing a discussion without replies" do
topic = @course.discussion_topics.create!(title: 'Unpublish me', user: @user)
topic.published?.should be_true
get(url)
click_publish_icon topic
topic.reload
topic.published?.should be_false
end
it "should not allow unpublishing a discussion with replies" do
topic = @course.discussion_topics.create!(title: 'Try to unpublish me', user: @user)
topic.reply_from user: @user, text: 'reply'
topic.published?.should be_true
get(url)
click_publish_icon topic
topic.reload
topic.published?.should be_true
end
it "should not allow unpublishing a graded discussion" do
group_discussion_assignment
@topic.published?.should be_true
get(url)
click_publish_icon @topic
@topic.reload
@topic.published?.should be_true
end
it "should allow publishing and unpublishing from a topic's page" do
topic = @course.discussion_topics.create!(title: 'Publish me', user: @user)
topic.workflow_state = 'post_delayed'
topic.save!
topic.published?.should be_false
get url + "#{topic.id}"
f('#topic_publish_button').click
wait_for_ajaximations
topic.reload
topic.published?.should be_true
f('#topic_publish_button').click
wait_for_ajaximations
topic.reload
topic.published?.should be_false
end
end
end
it "should allow teachers to edit discussions settings" do
@ -717,7 +785,7 @@ describe "discussions" do
get "/courses/#{@course.id}/discussion_topics/#{@topic.id}"
wait_for_ajaximations
f("#discussion-toolbar .al-trigger").click
f("#discussion-managebar .al-trigger").click
expect_new_page_load { f(".discussion_locked_toggler").click }
@topic.reload
@ -1229,7 +1297,7 @@ describe "discussions" do
ff('.discussion-entries .unread').length.should == reply_count
ff('.discussion-entries .read').length.should == 0
f("#discussion-toolbar .al-trigger").click
f("#discussion-managebar .al-trigger").click
f('.mark_all_as_read').click
wait_for_ajaximations
ff('.discussion-entries .unread').length.should == 0