mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -05:00
Staff can enter and view deleted topics
This commit is contained in:
parent
eba662b988
commit
19c169540c
24 changed files with 176 additions and 83 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'>
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>");
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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>");
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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' %>…</a>
|
<a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>…</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' %>…</a>
|
<a href="/latest" class="btn"><%= t 'page_not_found.see_more' %>…</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>
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 )}
|
||||||
|
|
|
@ -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");
|
||||||
});
|
});
|
Loading…
Reference in a new issue