Wiki Post

This commit is contained in:
Wojciech Zawistowski 2014-05-13 08:53:11 -04:00
parent 27c702464e
commit 960d64930c
26 changed files with 371 additions and 19 deletions

View File

@ -60,6 +60,21 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
}.property("viewMode", "category_changes"),
wiki_diff: function() {
var viewMode = this.get("viewMode");
var changes = this.get("wiki_changes");
if (changes === null) { return; }
if (viewMode === "inline") {
var diff = changes["current_wiki"] ? '<i class="fa fa-pencil-square-o fa-2x"></i>' : '<span class="fa-stack"><i class="fa fa-pencil-square-o fa-stack-2x"></i><i class="fa fa-ban fa-stack-2x"></i></span>';
return "<div class='inline-diff'>" + diff + "</div>";
} else {
var prev = changes["previous_wiki"] ? '<i class="fa fa-pencil-square-o fa-2x"></i>' : "&nbsp;";
var curr = changes["current_wiki"] ? '<i class="fa fa-pencil-square-o fa-2x"></i>' : '<span class="fa-stack"><i class="fa fa-pencil-square-o fa-stack-2x"></i><i class="fa fa-ban fa-stack-2x"></i></span>';
return "<div class='span8'>" + prev + "</div><div class='span8 offset1'>" + curr + "</div>";
}
}.property("viewMode", "wiki_changes"),
title_diff: function() {
var viewMode = this.get("viewMode");
if(viewMode === "side_by_side_markdown") {

View File

@ -243,6 +243,10 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}).finally(function() {
self.set('loadingExpanded', false);
});
},
toggleWiki: function(post) {
post.toggleProperty('wiki');
}
},

View File

@ -80,6 +80,25 @@ Discourse.Post = Discourse.Model.extend({
}.observes('bookmarked'),
wikiChanged: function() {
var self = this;
Discourse.ajax('/posts/' + this.get('id') + '/wiki', {
type: 'PUT',
data: {
wiki: this.get('wiki') ? true : false
}
}).then(function() {
self.incrementProperty('version');
}, function(error) {
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors[0]);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
}.observes('wiki'),
internalLinks: function() {
if (this.blank('link_counts')) return null;
return this.get('link_counts').filterProperty('internal').filterProperty('title');
@ -439,5 +458,3 @@ Discourse.Post.reopenClass({
}
});

View File

@ -35,6 +35,11 @@
{{boundAvatar user_changes.previous imageSize="small"}} {{user_changes.previous.username}}{{boundAvatar user_changes.current imageSize="small"}} {{user_changes.current.username}}
</div>
{{/if}}
{{#if wiki_changes}}
<div class="row">
{{{wiki_diff}}}
</div>
{{/if}}
{{{body_diff}}}
</div>
</div>

View File

@ -31,6 +31,9 @@
</div>
</div>
{{/unless}}
{{#if wiki}}
<div class="wiki"><i class="fa fa-pencil-square-o fa-3x"></i></div>
{{/if}}
</div>
<div class='topic-body span14'>

View File

@ -21,18 +21,17 @@ Discourse.PostMenuView = Discourse.View.extend({
'post.bookmarked',
'post.shareUrl',
'post.topic.deleted_at',
'post.replies.length'),
'post.replies.length',
'post.wiki'),
render: function(buffer) {
var post = this.get('post');
buffer.push("<nav class='post-controls'>");
this.renderReplies(post, buffer);
var postMenuView = this;
Discourse.get('postButtons').toArray().reverse().forEach(function(button) {
var renderer = "render" + button;
if(postMenuView[renderer]) postMenuView[renderer](post, buffer);
});
buffer.push("<nav class='post-controls'>");
this.renderReplies(post, buffer);
this.renderButtons(post, buffer);
buffer.push("</nav>");
},
@ -61,6 +60,14 @@ Discourse.PostMenuView = Discourse.View.extend({
return buffer.push("<i class='fa " + icon + "'></i></button>");
},
renderButtons: function(post, buffer) {
var self = this;
Discourse.get('postButtons').toArray().reverse().forEach(function(button) {
var renderer = "render" + button;
if(self[renderer]) self[renderer](post, buffer);
});
},
clickReplies: function() {
if (this.get('post.replies.length') > 0) {
this.set('post.replies', []);
@ -214,6 +221,34 @@ Discourse.PostMenuView = Discourse.View.extend({
clickBookmark: function() {
this.get('post').toggleProperty('bookmarked');
},
renderAdmin: function(post, buffer) {
var currentUser = Discourse.User.current();
if (!currentUser || !currentUser.get('canManageTopic')) {
return;
}
buffer.push('<button title="' + I18n.t("post.controls.admin") + '" data-action="admin" class="admin"><i class="fa fa-wrench"></i>');
this.renderAdminPopup(post, buffer);
buffer.push('</button>');
},
renderAdminPopup: function(post, buffer) {
var wikiText = post.get('wiki') ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki');
buffer.push('<div class="post-admin-menu"><h3>' + I18n.t('admin_title') + '</h3><ul><li class="btn btn-admin" data-action="toggleWiki"><i class="fa fa-pencil-square-o"></i>' + wikiText +'</li></ul></div>');
},
clickAdmin: function() {
var $adminMenu = this.$('.post-admin-menu');
this.set('postView.adminMenu', $adminMenu);
$adminMenu.show();
},
clickToggleWiki: function() {
this.get('controller').send('toggleWiki', this.get('post'));
}
});

View File

@ -43,6 +43,12 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
this.get('controller').toggledSelectedPost(this.get('post'));
}
var $adminMenu = this.get('adminMenu');
if ($adminMenu && !$(e.target).is($adminMenu) && $adminMenu.has($(e.target)).length === 0) {
$adminMenu.hide();
this.set('adminMenu', null);
}
},
selected: function() {

View File

@ -129,4 +129,7 @@
.modal-header {
height: 42px;
}
.fa-ban {
color: #f00;
}
}

View File

@ -168,7 +168,10 @@ nav.post-controls {
&.hidden {
display: none;
}
&.like, &.edit, &.flag, &.delete, &.share, &.bookmark, &.create {
&.admin {
position: relative;
}
&.like, &.edit, &.flag, &.delete, &.share, &.bookmark, &.create, &.admin {
float: right;
}
@ -203,6 +206,40 @@ nav.post-controls {
}
}
}
.post-admin-menu {
background-color: $secondary;
width: 205px;
padding: 10px;
border: 1px solid scale-color($primary, $lightness: 90%);
position: absolute;
text-align: left;
bottom: 0;
right: 0;
z-index: 1000;
display: none;
h3 {
margin-top: 0;
color: $primary;
font-size: 1em;
}
ul {
list-style: none;
margin: 10px 0 0 0;
}
li {
width: 176px;
margin-bottom: 5px;
i.fa {
width: 14px;
margin-right: 14px;
}
}
}
}
.embedded-posts {
@ -733,6 +770,11 @@ blockquote { /* solo quotes */
.topic-avatar {
border-top: 1px solid scale-color($primary, $lightness: 90%);
padding-top: 16px;
.wiki {
margin-top: 14px;
color: scale-color($primary, $lightness: 60%);
}
}
.bottom-round.contents.regular {

View File

@ -22,7 +22,11 @@ body {
height: 100%;
@include border-radius-all(5px);
.contents {
float: left;
padding: 10px 10px 10px 10px;
h3 {
float: left;
}
}
&.white {
background-color: $secondary;

View File

@ -54,7 +54,10 @@ button {
&.hidden {
display: none;
}
&.like, &.edit, &.flag, &.delete, &.share, &.bookmark, &.create {
&.admin {
position: relative;
}
&.like, &.edit, &.flag, &.delete, &.share, &.bookmark, &.create, &.admin {
float: right;
}
.read-icon {
@ -77,6 +80,38 @@ button {
}
}
.post-admin-menu {
background-color: $secondary;
width: 205px;
padding: 10px;
border: 1px solid scale-color($primary, $lightness: 90%);
position: absolute;
text-align: left;
bottom: 0;
left: 0;
z-index: 1000;
display: none;
h3 {
color: $primary;
font-size: 1em;
}
ul {
list-style: none;
margin: 10px 0 0 0;
}
li {
width: 176px;
margin-bottom: 5px;
i.fa {
width: 14px;
margin-right: 14px;
}
}
}
.embedded-posts {
@ -353,6 +388,19 @@ iframe {
word-wrap: break-word;
}
.wiki {
float: left;
padding: 10px;
color: #408040;
i {
float: left;
}
h3 {
float: left;
margin-left: 10px;
}
}
.modal-body {
input[type=text] {
font-size: 16px;

View File

@ -208,6 +208,17 @@ class PostsController < ApplicationController
render nothing: true
end
def wiki
guardian.ensure_can_wiki!
post = find_post_from_params
post.wiki = params[:wiki]
post.version += 1
post.save
render nothing: true
end
protected
def find_post_revision_from_params

View File

@ -478,7 +478,7 @@ class Post < ActiveRecord::Base
end
def save_revision
modifications = changes.extract!(:raw, :cooked, :edit_reason, :user_id)
modifications = changes.extract!(:raw, :cooked, :edit_reason, :user_id, :wiki)
# make sure cooked is always present (oneboxes might not change the cooked post)
modifications["cooked"] = [self.cooked, self.cooked] unless modifications["cooked"].present?
PostRevision.create!(

View File

@ -28,6 +28,17 @@ class PostRevision < ActiveRecord::Base
}
end
def wiki_changes
prev = lookup("wiki", 0)
cur = lookup("wiki", 1)
return if prev == cur
{
previous_wiki: prev,
current_wiki: cur,
}
end
def title_changes
prev = "<div>#{CGI::escapeHTML(previous("title"))}</div>"
cur = "<div>#{CGI::escapeHTML(current("title"))}</div>"
@ -79,7 +90,7 @@ class PostRevision < ActiveRecord::Base
end
unless val
if ["cooked","raw"].include?(field)
if ["cooked", "raw"].include?(field)
val = post.send(field)
else
val = post.topic.send(field)

View File

@ -10,7 +10,8 @@ class PostRevisionSerializer < ApplicationSerializer
:body_changes,
:title_changes,
:category_changes,
:user_changes
:user_changes,
:wiki_changes
def include_title_changes?
object.has_topic_data?

View File

@ -47,7 +47,8 @@ class PostSerializer < BasicPostSerializer
:deleted_by,
:user_deleted,
:edit_reason,
:can_view_edit_history
:can_view_edit_history,
:wiki
def moderator?

View File

@ -966,6 +966,9 @@ en:
other: "Do you also want to delete the {{count}} direct replies to this post?"
yes_value: "Yes, delete the replies too"
no_value: "No, just this post"
admin: "post admin actions"
wiki: "Wiki post"
unwiki: "Unwiki post"
actions:
flag: 'Flag'

View File

@ -780,6 +780,8 @@ en:
min_trust_to_create_topic: "The minimum trust level required to create a new topic."
min_trust_to_edit_wiki_post: "The minimum trust level required to edit post marked as wiki."
newuser_max_links: "How many links a new user can add to a post"
newuser_max_images: "How many images a new user can add to a post"
newuser_max_attachments: "How many attachments a new user can add to a post"

View File

@ -225,6 +225,7 @@ Discourse::Application.routes.draw do
resources :posts do
put "bookmark"
put "wiki"
get "replies"
get "revisions/:revision" => "posts#revisions"
put "recover"

View File

@ -58,7 +58,7 @@ basic:
post_menu:
client: true
list: true
default: 'like|edit|flag|delete|share|bookmark|reply'
default: 'like|edit|flag|delete|share|bookmark|admin|reply'
share_links:
client: true
list: true
@ -333,6 +333,9 @@ trust:
min_trust_to_create_topic:
default: 0
enum: 'MinTrustToCreateTopicSetting'
min_trust_to_edit_wiki_post:
default: 0
enum: 'MinTrustToCreateTopicSetting'
basic_requires_topics_entered: 5
basic_requires_read_posts: 50
basic_requires_time_spent_mins: 15

View File

@ -0,0 +1,5 @@
class AddWikiToPosts < ActiveRecord::Migration
def change
add_column :posts, :wiki, :boolean, default: false, null: false
end
end

View File

@ -68,7 +68,23 @@ module PostGuardian
# Editing Method
def can_edit_post?(post)
is_staff? || @user.has_trust_level?(:elder) || (!post.topic.archived? && is_my_own?(post) && !post.user_deleted && !post.deleted_at && !post.edit_time_limit_expired?)
if is_staff? || @user.has_trust_level?(:elder)
return true
end
if post.topic.archived? || post.user_deleted || post.deleted_at
return false
end
if post.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i)
return true
end
if is_my_own?(post) && !post.edit_time_limit_expired?
return true
end
false
end
# Deleting Methods
@ -129,4 +145,8 @@ module PostGuardian
def can_change_post_owner?
is_admin?
end
def can_wiki?
is_staff? || @user.has_trust_level?(:elder)
end
end

View File

@ -617,6 +617,11 @@ describe Guardian do
Guardian.new.can_edit?(post).should be_false
end
it 'returns false when not logged in also for wiki post' do
post.wiki = true
Guardian.new.can_edit?(post).should be_false
end
it 'returns true if you want to edit your own post' do
Guardian.new(post.user).can_edit?(post).should be_true
end
@ -626,15 +631,32 @@ describe Guardian do
Guardian.new(post.user).can_edit?(post).should be_false
end
it 'returns false if another regular user tries to edit a soft deleted wiki post' do
post.wiki = true
post.user_deleted = true
Guardian.new(coding_horror).can_edit?(post).should be_false
end
it 'returns false if you are trying to edit a deleted post' do
post.deleted_at = 1.day.ago
Guardian.new(post.user).can_edit?(post).should be_false
end
it 'returns false if another regular user tries to edit a deleted wiki post' do
post.wiki = true
post.deleted_at = 1.day.ago
Guardian.new(coding_horror).can_edit?(post).should be_false
end
it 'returns false if another regular user tries to edit your post' do
Guardian.new(coding_horror).can_edit?(post).should be_false
end
it 'returns true if another regular user tries to edit wiki post' do
post.wiki = true
Guardian.new(coding_horror).can_edit?(post).should be_true
end
it 'returns true as a moderator' do
Guardian.new(moderator).can_edit?(post).should be_true
end
@ -668,6 +690,35 @@ describe Guardian do
it 'returns false for another regular user trying to edit your post' do
Guardian.new(coding_horror).can_edit?(old_post).should == false
end
it 'returns true for another regular user trying to edit a wiki post' do
old_post.wiki = true
Guardian.new(coding_horror).can_edit?(old_post).should be_true
end
it 'returns false when another user has too low trust level to edit wiki post' do
SiteSetting.stubs(:min_trust_to_edit_wiki_post).returns(2)
post.wiki = true
coding_horror.trust_level = 1
Guardian.new(coding_horror).can_edit?(post).should be_false
end
it 'returns true when another user has adequate trust level to edit wiki post' do
SiteSetting.stubs(:min_trust_to_edit_wiki_post).returns(2)
post.wiki = true
coding_horror.trust_level = 2
Guardian.new(coding_horror).can_edit?(post).should be_true
end
it 'returns true for post author even when he has too low trust level to edit wiki post' do
SiteSetting.stubs(:min_trust_to_edit_wiki_post).returns(2)
post.wiki = true
post.user.trust_level = 1
Guardian.new(post.user).can_edit?(post).should be_true
end
end
end
@ -1659,5 +1710,19 @@ describe Guardian do
end
end
end
describe 'can_wiki?' do
it 'returns false for regular user' do
Guardian.new(coding_horror).can_wiki?.should be_false
end
it 'returns true for admin user' do
Guardian.new(admin).can_wiki?.should be_true
end
it 'returns true for elder user' do
Guardian.new(elder).can_wiki?.should be_true
end
end
end

View File

@ -316,6 +316,45 @@ describe PostsController do
end
describe "wiki" do
include_examples "action requires login", :put, :wiki, post_id: 2
describe "when logged in" do
let(:user) {log_in}
let(:post) {Fabricate(:post, user: user)}
it "raises an error if the user doesn't have permission to see the post" do
Guardian.any_instance.expects(:can_wiki?).returns(false)
xhr :put, :wiki, post_id: post.id, wiki: 'true'
response.should be_forbidden
end
it "can wiki a post" do
Guardian.any_instance.expects(:can_wiki?).returns(true)
xhr :put, :wiki, post_id: post.id, wiki: 'true'
post.reload
post.wiki.should be_true
end
it "can unwiki a post" do
wikied_post = Fabricate(:post, user: user, wiki: true)
Guardian.any_instance.expects(:can_wiki?).returns(true)
xhr :put, :wiki, post_id: wikied_post.id, wiki: 'false'
wikied_post.reload
wikied_post.wiki.should be_false
end
end
end
describe 'creating a post' do
include_examples 'action requires login', :post, :create

View File

@ -62,4 +62,12 @@ describe PostRevision do
end
it "can find wiki changes" do
r = create_rev("wiki" => [false, true])
changes = r.wiki_changes
changes[:previous_wiki].should be_false
changes[:current_wiki].should be_true
end
end

View File

@ -1,3 +1,3 @@
/*jshint maxlen:10000000 */
Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|starred|categories","post_menu":"like|edit|flag|delete|share|bookmark|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"min_password_length":8,"enable_local_logins":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif|.txt","relative_date_duration":14,"delete_removed_posts_after":24,"delete_user_max_post_age":7, "default_code_lang": "lang-auto", "suppress_uncategorized_badge": true};
Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|starred|categories","post_menu":"like|edit|flag|delete|share|bookmark|admin|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"min_password_length":8,"enable_local_logins":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif|.txt","relative_date_duration":14,"delete_removed_posts_after":24,"delete_user_max_post_age":7, "default_code_lang": "lang-auto", "suppress_uncategorized_badge": true};
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);