Staff can enter and view deleted topics

This commit is contained in:
Robin Ward 2013-07-11 16:38:46 -04:00
parent eba662b988
commit 19c169540c
24 changed files with 176 additions and 83 deletions

View file

@ -60,6 +60,8 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
return canDelete; return canDelete;
}.property('selectedPostsCount'), }.property('selectedPostsCount'),
hasError: Ember.computed.or('errorBodyHtml', 'message'),
streamPercentage: function() { streamPercentage: function() {
if (!this.get('postStream.loaded')) { return 0; } if (!this.get('postStream.loaded')) { return 0; }
if (this.get('postStream.filteredPostsCount') === 0) { return 0; } if (this.get('postStream.filteredPostsCount') === 0) { return 0; }
@ -248,12 +250,8 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}.property('isPrivateMessage'), }.property('isPrivateMessage'),
deleteTopic: function() { deleteTopic: function() {
var topicController = this;
this.unsubscribe(); this.unsubscribe();
this.get('content').destroy().then(function() { this.get('content').destroy(Discourse.User.current());
topicController.set('message', I18n.t('topic.deleted'));
topicController.set('loaded', false);
});
}, },
toggleVisibility: function() { toggleVisibility: function() {

View file

@ -258,7 +258,7 @@ Discourse.PostStream = Em.Object.extend({
Discourse.URL.set('queryParams', postStream.get('streamFilters')); Discourse.URL.set('queryParams', postStream.get('streamFilters'));
}, function(result) { }, function(result) {
postStream.errorLoading(result.status); postStream.errorLoading(result);
}); });
}, },
hasLoadedData: Em.computed.and('hasPosts', 'hasStream'), hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),
@ -612,7 +612,8 @@ Discourse.PostStream = Em.Object.extend({
@param {Integer} status the HTTP status code @param {Integer} status the HTTP status code
@param {Discourse.Topic} topic The topic instance we were trying to load @param {Discourse.Topic} topic The topic instance we were trying to load
**/ **/
errorLoading: function(status) { errorLoading: function(result) {
var status = result.status;
var topic = this.get('topic'); var topic = this.get('topic');
topic.set('loadingFilter', false); topic.set('loadingFilter', false);
@ -621,7 +622,7 @@ Discourse.PostStream = Em.Object.extend({
// If the result was 404 the post is not found // If the result was 404 the post is not found
if (status === 404) { if (status === 404) {
topic.set('errorTitle', I18n.t('topic.not_found.title')); topic.set('errorTitle', I18n.t('topic.not_found.title'));
topic.set('message', I18n.t('topic.not_found.description')); topic.set('errorBodyHtml', result.responseText);
return; return;
} }

View file

@ -17,6 +17,7 @@ Discourse.Topic = Discourse.Model.extend({
}.property(), }.property(),
invisible: Em.computed.not('visible'), invisible: Em.computed.not('visible'),
deleted: Em.computed.notEmpty('deleted_at'),
canConvertToRegular: function() { canConvertToRegular: function() {
var a = this.get('archetype'); var a = this.get('archetype');
@ -142,13 +143,13 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
favoriteTooltipKey: (function() { favoriteTooltipKey: function() {
return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star'; return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star';
}).property('starred'), }.property('starred'),
favoriteTooltip: (function() { favoriteTooltip: function() {
return I18n.t(this.get('favoriteTooltipKey')); return I18n.t(this.get('favoriteTooltipKey'));
}).property('favoriteTooltipKey'), }.property('favoriteTooltipKey'),
toggleStar: function() { toggleStar: function() {
var topic = this; var topic = this;
@ -181,22 +182,26 @@ Discourse.Topic = Discourse.Model.extend({
// Reset our read data for this topic // Reset our read data for this topic
resetRead: function() { resetRead: function() {
return Discourse.ajax("/t/" + (this.get('id')) + "/timings", { return Discourse.ajax("/t/" + this.get('id') + "/timings", {
type: 'DELETE' type: 'DELETE'
}); });
}, },
// Invite a user to this topic // Invite a user to this topic
inviteUser: function(user) { inviteUser: function(user) {
return Discourse.ajax("/t/" + (this.get('id')) + "/invite", { return Discourse.ajax("/t/" + this.get('id') + "/invite", {
type: 'POST', type: 'POST',
data: { user: user } data: { user: user }
}); });
}, },
// Delete this topic // Delete this topic
destroy: function() { destroy: function(deleted_by) {
return Discourse.ajax("/t/" + (this.get('id')), { type: 'DELETE' }); this.setProperties({
deleted_at: new Date(),
deleted_by: deleted_by
});
return Discourse.ajax("/t/" + this.get('id'), { type: 'DELETE' });
}, },
// Update our attributes from a JSON result // Update our attributes from a JSON result

View file

@ -107,15 +107,18 @@
</div> </div>
{{else}} {{else}}
{{#if message}} {{#if hasError}}
<div class='container'> <div class='container'>
<div class='message'> {{#if errorBodyHtml}}
{{{errorBodyHtml}}}
{{/if}}
<h2>{{message}}</h2> {{#if message}}
<div class="message">
<h2>{{message}}</h2>
</div>
{{/if}}
<p>
{{#linkTo list.latest}}{{i18n topic.back_to_list}}{{/linkTo}}
</div>
</div> </div>
{{else}} {{else}}
<div class='container'> <div class='container'>

View file

@ -9,7 +9,7 @@
Discourse.ButtonView = Discourse.View.extend({ Discourse.ButtonView = Discourse.View.extend({
tagName: 'button', tagName: 'button',
classNameBindings: [':btn', ':standard', 'dropDownToggle'], classNameBindings: [':btn', ':standard', 'dropDownToggle'],
attributeBindings: ['data-not-implemented', 'title', 'data-toggle', 'data-share-url'], attributeBindings: ['title', 'data-toggle', 'data-share-url'],
title: function() { title: function() {
return I18n.t(this.get('helpKey') || this.get('textKey')); return I18n.t(this.get('helpKey') || this.get('textKey'));

View file

@ -7,9 +7,7 @@
@module Discourse @module Discourse
**/ **/
Discourse.DropdownButtonView = Discourse.View.extend({ Discourse.DropdownButtonView = Discourse.View.extend({
classNames: ['btn-group'], classNameBindings: [':btn-group', 'hidden'],
attributeBindings: ['data-not-implemented'],
shouldRerender: Discourse.View.renderIfChanged('text', 'longDescription'), shouldRerender: Discourse.View.renderIfChanged('text', 'longDescription'),
didInsertElement: function(e) { didInsertElement: function(e) {

View file

@ -8,9 +8,10 @@
**/ **/
Discourse.FavoriteButton = Discourse.ButtonView.extend({ Discourse.FavoriteButton = Discourse.ButtonView.extend({
textKey: 'favorite.title', textKey: 'favorite.title',
helpKeyBinding: 'controller.content.favoriteTooltipKey', helpKeyBinding: 'controller.favoriteTooltipKey',
attributeBindings: ['disabled'],
shouldRerender: Discourse.View.renderIfChanged('controller.content.starred'), shouldRerender: Discourse.View.renderIfChanged('controller.starred'),
click: function() { click: function() {
this.get('controller').toggleStar(); this.get('controller').toggleStar();
@ -18,7 +19,7 @@ Discourse.FavoriteButton = Discourse.ButtonView.extend({
renderIcon: function(buffer) { renderIcon: function(buffer) {
buffer.push("<i class='icon-star " + buffer.push("<i class='icon-star " +
(this.get('controller.content.starred') ? ' starred' : '') + (this.get('controller.starred') ? ' starred' : '') +
"'></i>"); "'></i>");
} }
}); });

View file

@ -10,7 +10,7 @@ Discourse.InviteReplyButton = Discourse.ButtonView.extend({
textKey: 'topic.invite_reply.title', textKey: 'topic.invite_reply.title',
helpKey: 'topic.invite_reply.help', helpKey: 'topic.invite_reply.help',
attributeBindings: ['disabled'], attributeBindings: ['disabled'],
disabled: Em.computed.or('controller.content.archived', 'controller.content.closed'), disabled: Em.computed.or('controller.archived', 'controller.closed', 'controller.deleted'),
renderIcon: function(buffer) { renderIcon: function(buffer) {
buffer.push("<i class='icon icon-group'></i>"); buffer.push("<i class='icon icon-group'></i>");

View file

@ -9,6 +9,8 @@
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
title: I18n.t('topic.notifications.title'), title: I18n.t('topic.notifications.title'),
longDescriptionBinding: 'topic.details.notificationReasonText', longDescriptionBinding: 'topic.details.notificationReasonText',
topic: Em.computed.alias('controller.model'),
hidden: Em.computed.alias('topic.deleted'),
dropDownContent: [ dropDownContent: [
[Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'], [Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'],

View file

@ -10,6 +10,7 @@ Discourse.ShareButton = Discourse.ButtonView.extend({
textKey: 'topic.share.title', textKey: 'topic.share.title',
helpKey: 'topic.share.help', helpKey: 'topic.share.help',
'data-share-url': Em.computed.alias('topic.shareUrl'), 'data-share-url': Em.computed.alias('topic.shareUrl'),
topic: Em.computed.alias('controller.model'),
renderIcon: function(buffer) { renderIcon: function(buffer) {
buffer.push("<i class='icon icon-share'></i>"); buffer.push("<i class='icon icon-share'></i>");

View file

@ -26,13 +26,13 @@ Discourse.TopicFooterButtonsView = Discourse.ContainerView.extend({
this.attachViewClass(Discourse.InviteReplyButton); this.attachViewClass(Discourse.InviteReplyButton);
} }
this.attachViewClass(Discourse.FavoriteButton); this.attachViewClass(Discourse.FavoriteButton);
this.attachViewWithArgs({topic: topic}, Discourse.ShareButton); this.attachViewClass(Discourse.ShareButton);
this.attachViewClass(Discourse.ClearPinButton); this.attachViewClass(Discourse.ClearPinButton);
} }
this.attachViewClass(Discourse.ReplyButton); this.attachViewClass(Discourse.ReplyButton);
if (!topic.get('isPrivateMessage')) { if (!topic.get('isPrivateMessage')) {
this.attachViewWithArgs({topic: topic}, Discourse.NotificationsButton); this.attachViewClass(Discourse.NotificationsButton);
} }
this.trigger('additionalButtons', this); this.trigger('additionalButtons', this);
} else { } else {

View file

@ -9,15 +9,17 @@
**/ **/
Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
templateName: 'topic', templateName: 'topic',
topicBinding: 'controller.content', topicBinding: 'controller.model',
userFiltersBinding: 'controller.userFilters', userFiltersBinding: 'controller.userFilters',
classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.category.secure:secure_category'], classNameBindings: ['controller.multiSelect:multi-select',
'topic.archetype',
'topic.category.secure:secure_category',
'topic.deleted:deleted-topic'],
menuVisible: true, menuVisible: true,
SHORT_POST: 1200, SHORT_POST: 1200,
postStream: Em.computed.alias('controller.postStream'), postStream: Em.computed.alias('controller.postStream'),
updateBar: function() { updateBar: function() {
var $topicProgress = $('#topic-progress'); var $topicProgress = $('#topic-progress');
if (!$topicProgress.length) return; if (!$topicProgress.length) return;
@ -168,7 +170,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
// Called for every post seen, returns the post number // Called for every post seen, returns the post number
postSeen: function($post) { postSeen: function($post) {
var post = this.getPost($post); var post = this.getPost($post);
if (post) { if (post) {

View file

@ -777,7 +777,7 @@
} }
} }
// Private messages // Custom Gutter Glyphs
// -------------------------------------------------- // --------------------------------------------------
.private_message .gutter { .private_message .gutter {
@ -794,6 +794,20 @@
} }
.deleted-topic .gutter {
position: relative;
&:before {
display: block;
position: absolute;
top: 0;
left: 0;
color: rgba($black, 0.05);
font: 90px/1 FontAwesome;
content: "\f05c";
}
}
.secure_category .gutter { .secure_category .gutter {
position: relative; position: relative;
&:before { &:before {

View file

@ -74,9 +74,9 @@ class ApplicationController < ActionController::Base
def rescue_discourse_actions(message, error) def rescue_discourse_actions(message, error)
if request.format && request.format.json? if request.format && request.format.json?
render status: error, layout: false, text: message render status: error, layout: false, text: (error == 404) ? build_not_found_page(error) : message
else else
render_not_found_page(error) render text: build_not_found_page(error, 'no_js')
end end
end end
@ -123,7 +123,6 @@ class ApplicationController < ActionController::Base
@guardian ||= Guardian.new(current_user) @guardian ||= Guardian.new(current_user)
end end
def serialize_data(obj, serializer, opts={}) def serialize_data(obj, serializer, opts={})
# If it's an array, apply the serializer as an each_serializer to the elements # If it's an array, apply the serializer as an each_serializer to the elements
serializer_opts = {scope: guardian}.merge!(opts) serializer_opts = {scope: guardian}.merge!(opts)
@ -261,13 +260,13 @@ class ApplicationController < ActionController::Base
redirect_to :login if SiteSetting.login_required? && !current_user redirect_to :login if SiteSetting.login_required? && !current_user
end end
def render_not_found_page(status=404) def build_not_found_page(status=404, layout=false)
@top_viewed = TopicQuery.top_viewed(10) @top_viewed = TopicQuery.top_viewed(10)
@recent = TopicQuery.recent(10) @recent = TopicQuery.recent(10)
@slug = params[:slug].class == String ? params[:slug] : '' @slug = params[:slug].class == String ? params[:slug] : ''
@slug = (params[:id].class == String ? params[:id] : '') if @slug.blank? @slug = (params[:id].class == String ? params[:id] : '') if @slug.blank?
@slug.gsub!('-',' ') @slug.gsub!('-',' ')
render status: status, layout: 'no_js', formats: [:html], template: '/exceptions/not_found' render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
end end
protected protected

View file

@ -25,6 +25,7 @@ class TopicsController < ApplicationController
caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" } caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
def show def show
# We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with # We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with
# existing installs. # existing installs.
return wordpress if params[:best].present? return wordpress if params[:best].present?
@ -33,6 +34,7 @@ class TopicsController < ApplicationController
begin begin
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts) @topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
rescue Discourse::NotFound rescue Discourse::NotFound
Rails.logger.info ">>>> B"
topic = Topic.where(slug: params[:id]).first if params[:id] topic = Topic.where(slug: params[:id]).first if params[:id]
raise Discourse::NotFound unless topic raise Discourse::NotFound unless topic
return redirect_to(topic.relative_url) return redirect_to(topic.relative_url)

View file

@ -19,7 +19,8 @@ class TopicViewSerializer < ApplicationSerializer
:has_best_of, :has_best_of,
:archetype, :archetype,
:slug, :slug,
:category_id] :category_id,
:deleted_at]
end end
attributes :draft, attributes :draft,

View file

@ -1,31 +1,43 @@
<% local_domain = "#{request.protocol}#{request.host_with_port}" %> <% local_domain = "#{request.protocol}#{request.host_with_port}" %>
<p><%= t 'page_not_found.title' %></p> <p><%= t 'page_not_found.title' %></p>
<table>
<tr> <div class="row">
<td style="vertical-align:top; padding:0 20px 20px 0;"> <div class="span8">
<h2><%= t 'page_not_found.popular_topics' %></h2> <h2><%= t 'page_not_found.popular_topics' %></h2>
<% @top_viewed.each do |t| %> <% @top_viewed.each do |t| %>
<%= link_to t.title, t.relative_url %><br/> <%= link_to t.title, t.relative_url %><br/>
<% end %> <% end %>
<br/> <br/>
<a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>&hellip;</a> <a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>&hellip;</a>
</td> </div>
<td style="vertical-align:top; padding:0 0 20px 0;"> <div class="span8">
<h2><%= t 'page_not_found.recent_topics' %></h2> <h2><%= t 'page_not_found.recent_topics' %></h2>
<% @recent.each do |t| %> <% @recent.each do |t| %>
<%= link_to t.title, t.relative_url %><br/> <%= link_to t.title, t.relative_url %><br/>
<% end %> <% end %>
<br/> <br/>
<a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>&hellip;</a> <a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>&hellip;</a>
</td> </div>
</tr> </div>
</table>
<h2><%= t 'page_not_found.search_title' %></h2> <div class="row">
<p> <div class="span10" style='padding-top: 20px'>
<form action='http://google.com'> <h2><%= t 'page_not_found.search_title' %></h2>
<input type="text" name='q' value="site:<%= local_domain %> <%= @slug %>"> <p>
<!--<input type="button" class="btn" value="Search Here" onclick="alert('single page search results not implemented yet');" />--> <form action='http://google.com' id='google-search' onsubmit="return google_button_clicked()">
<input type="submit" class="btn btn-primary" value="<%= t 'page_not_found.search_google' %>" /> <input type="text" id='user-query' value="<%= @slug %>">
</form> <input type='hidden' id='google-query' name="q">
</p> <button class="btn btn-primary"><%= t 'page_not_found.search_google' %></button>
</form>
</p>
</div>
</div>
<script language="Javascript">
function google_button_clicked(e) {
var searchValue = document.getElementById('user-query').value;
document.getElementById('google-query').value = 'site:<%= local_domain %> ' + searchValue;
return true;
}
</script>

View file

@ -1052,7 +1052,7 @@ en:
access_token_problem: "Tell an admin: Please update the site settings to include the correct discourse_org_access_key." access_token_problem: "Tell an admin: Please update the site settings to include the correct discourse_org_access_key."
page_not_found: page_not_found:
title: "The page you requested doesn't exist on this discussion forum. Perhaps we can help find it, or another topic like it:" title: "The page you requested doesn't exist or may have been deleted by a moderator."
popular_topics: "Popular topics" popular_topics: "Popular topics"
recent_topics: "Recent topics" recent_topics: "Recent topics"
see_more: "See More" see_more: "See More"

View file

@ -242,7 +242,11 @@ class Guardian
end end
def can_create_post_on_topic?(topic) def can_create_post_on_topic?(topic)
is_staff? || (not(topic.closed? || topic.archived?) && can_create_post?(topic))
# No users can create posts on deleted topics
return false if topic.trashed?
is_staff? || (not(topic.closed? || topic.archived? || topic.trashed?) && can_create_post?(topic))
end end
# Editing Methods # Editing Methods
@ -283,7 +287,9 @@ class Guardian
end end
def can_delete_topic?(topic) def can_delete_topic?(topic)
is_staff? && not(Category.exists?(topic_id: topic.id)) !topic.trashed? &&
is_staff? &&
!(Category.exists?(topic_id: topic.id))
end end
def can_delete_post_action?(post_action) def can_delete_post_action?(post_action)

View file

@ -8,17 +8,18 @@ class TopicView
attr_accessor :draft, :draft_key, :draft_sequence attr_accessor :draft, :draft_key, :draft_sequence
def initialize(topic_id, user=nil, options={}) def initialize(topic_id, user=nil, options={})
@user = user
@topic = find_topic(topic_id) @topic = find_topic(topic_id)
raise Discourse::NotFound if @topic.blank? raise Discourse::NotFound if @topic.blank?
@guardian = Guardian.new(user) @guardian = Guardian.new(@user)
# Special case: If the topic is private and the user isn't logged in, ask them # Special case: If the topic is private and the user isn't logged in, ask them
# to log in! # to log in!
if @topic.present? && @topic.private_message? && user.blank? if @topic.present? && @topic.private_message? && @user.blank?
raise Discourse::NotLoggedIn.new raise Discourse::NotLoggedIn.new
end end
guardian.ensure_can_see!(@topic) guardian.ensure_can_see!(@topic)
@post_number, @page = options[:post_number], options[:page].to_i @post_number, @page = options[:post_number], options[:page].to_i
@ -36,14 +37,13 @@ class TopicView
@filtered_posts = @filtered_posts.where('post_number = 1 or user_id in (select u.id from users u where username_lower in (?))', usernames) @filtered_posts = @filtered_posts.where('post_number = 1 or user_id in (select u.id from users u where username_lower in (?))', usernames)
end end
@user = user
@initial_load = true @initial_load = true
@index_reverse = false @index_reverse = false
filter_posts(options) filter_posts(options)
@draft_key = @topic.draft_key @draft_key = @topic.draft_key
@draft_sequence = DraftSequence.current(user, @draft_key) @draft_sequence = DraftSequence.current(@user, @draft_key)
end end
def canonical_path def canonical_path
@ -317,6 +317,8 @@ class TopicView
end end
def find_topic(topic_id) def find_topic(topic_id)
Topic.where(id: topic_id).includes(:category).first finder = Topic.where(id: topic_id).includes(:category)
finder = finder.with_deleted if @user.try(:staff?)
finder.first
end end
end end

View file

@ -5,7 +5,6 @@ module Trashable
default_scope where(with_deleted_scope_sql) default_scope where(with_deleted_scope_sql)
# scope unscoped does not work # scope unscoped does not work
belongs_to :deleted_by, class_name: 'User' belongs_to :deleted_by, class_name: 'User'
end end
@ -26,6 +25,10 @@ module Trashable
end end
end end
def trashed?
deleted_at.present?
end
def trash!(trashed_by=nil) def trash!(trashed_by=nil)
# note, an argument could be made that the column should probably called trashed_at # note, an argument could be made that the column should probably called trashed_at
# however, deleted_at is the terminology used in the UI # however, deleted_at is the terminology used in the UI

View file

@ -318,7 +318,6 @@ describe Guardian do
end end
context 'regular users' do context 'regular users' do
it "doesn't allow new posts from regular users" do it "doesn't allow new posts from regular users" do
Guardian.new(coding_horror).can_create?(Post, topic).should be_false Guardian.new(coding_horror).can_create?(Post, topic).should be_false
end end
@ -326,7 +325,6 @@ describe Guardian do
it 'allows editing of posts' do it 'allows editing of posts' do
Guardian.new(coding_horror).can_edit?(post).should be_false Guardian.new(coding_horror).can_edit?(post).should be_false
end end
end end
it "allows new posts from moderators" do it "allows new posts from moderators" do
@ -338,6 +336,26 @@ describe Guardian do
end end
end end
context "trashed topic" do
before do
topic.deleted_at = Time.now
end
it "doesn't allow new posts from regular users" do
Guardian.new(coding_horror).can_create?(Post, topic).should be_false
end
it "doesn't allow new posts from moderators users" do
Guardian.new(moderator).can_create?(Post, topic).should be_false
end
it "doesn't allow new posts from admins" do
Guardian.new(admin).can_create?(Post, topic).should be_false
end
end
end end
end end

View file

@ -18,6 +18,14 @@ describe TopicView do
lambda { topic_view }.should raise_error(Discourse::InvalidAccess) lambda { topic_view }.should raise_error(Discourse::InvalidAccess)
end end
it "handles deleted topics" do
topic.trash!(coding_horror)
lambda { TopicView.new(topic.id, coding_horror) }.should raise_error(Discourse::NotFound)
coding_horror.stubs(:staff?).returns(true)
lambda { TopicView.new(topic.id, coding_horror) }.should_not raise_error
end
context "with a few sample posts" do context "with a few sample posts" do
let!(:p1) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 1 )} let!(:p1) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 1 )}
let!(:p2) { Fabricate(:post, topic: topic, user: coding_horror, percent_rank: 0.5 )} let!(:p2) { Fabricate(:post, topic: topic, user: coding_horror, percent_rank: 0.5 )}

View file

@ -1,5 +1,11 @@
module("Discourse.Topic"); module("Discourse.Topic");
test("defaults", function() {
var topic = Discourse.Topic.create({id: 1234});
blank(topic.get('deleted_at'), 'deleted_at defaults to blank');
blank(topic.get('deleted_by'), 'deleted_by defaults to blank');
});
test('has details', function() { test('has details', function() {
var topic = Discourse.Topic.create({id: 1234}); var topic = Discourse.Topic.create({id: 1234});
var topicDetails = topic.get('details'); var topicDetails = topic.get('details');
@ -36,4 +42,16 @@ test("updateFromJson", function() {
equal(topic.get('details.hello'), 'world', 'it updates the details'); equal(topic.get('details.hello'), 'world', 'it updates the details');
equal(topic.get('cool'), "property", "it updates other properties"); equal(topic.get('cool'), "property", "it updates other properties");
equal(topic.get('category'), category); equal(topic.get('category'), category);
});
test("destroy", function() {
var topic = Discourse.Topic.create({id: 1234});
var user = Discourse.User.create({username: 'eviltrout'});
this.stub(Discourse, 'ajax');
topic.destroy(user);
present(topic.get('deleted_at'), 'deleted at is set');
equal(topic.get('deleted_by'), user, 'deleted by is set');
ok(Discourse.ajax.calledOnce, "it called delete over the wire");
}); });