Merge pull request #4522 from fantasticfears/featured-link

FEATURE: Allow posting a link with the topic
This commit is contained in:
Neil Lalonde 2016-12-05 16:05:38 -05:00 committed by GitHub
commit 56ee4ffadc
50 changed files with 503 additions and 77 deletions

View File

@ -13,7 +13,8 @@ export default Ember.Component.extend({
'composer.canEditTitle:edit-title', 'composer.canEditTitle:edit-title',
'composer.createdPost:created-post', 'composer.createdPost:created-post',
'composer.creatingTopic:topic', 'composer.creatingTopic:topic',
'composer.whisper:composing-whisper'], 'composer.whisper:composing-whisper',
'composer.showComposerEditor::topic-featured-link-only'],
@computed('composer.composeState') @computed('composer.composeState')
composeState(composeState) { composeState(composeState) {
@ -27,7 +28,7 @@ export default Ember.Component.extend({
this.appEvents.trigger("composer:resized"); this.appEvents.trigger("composer:resized");
}, },
@observes('composeState', 'composer.action') @observes('composeState', 'composer.action', 'composer.canEditTopicFeaturedLink')
resize() { resize() {
Ember.run.scheduleOnce('afterRender', () => { Ember.run.scheduleOnce('afterRender', () => {
if (!this.element || this.isDestroying || this.isDestroyed) { return; } if (!this.element || this.isDestroying || this.isDestroyed) { return; }

View File

@ -21,6 +21,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.site.mobileView) { this.set("viewMode", "inline"); } if (this.site.mobileView) { this.set("viewMode", "inline"); }
}.on("init"), }.on("init"),
previousFeaturedLink: Em.computed.alias('model.featured_link_changes.previous'),
currentFeaturedLink: Em.computed.alias('model.featured_link_changes.current'),
previousTagChanges: customTagArray('model.tags_changes.previous'), previousTagChanges: customTagArray('model.tags_changes.previous'),
currentTagChanges: customTagArray('model.tags_changes.current'), currentTagChanges: customTagArray('model.tags_changes.current'),

View File

@ -160,6 +160,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return post => this.postSelected(post); return post => this.postSelected(post);
}.property(), }.property(),
@computed('model.isPrivateMessage', 'model.category.id')
canEditTopicFeaturedLink(isPrivateMessage, categoryId) {
if (!this.siteSettings.topic_featured_link_enabled || isPrivateMessage) { return false; }
const categoryIds = this.site.get('topic_featured_link_allowed_category_ids');
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
},
@computed('model.isPrivateMessage') @computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) { canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics'); return !isPrivateMessage && this.site.get('can_tag_topics');

View File

@ -0,0 +1,6 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import renderTopicFeaturedLink from 'discourse/lib/render-topic-featured-link';
export default registerUnbound('topic-featured-link', function(topic, params) {
return new Handlebars.SafeString(renderTopicFeaturedLink(topic, params));
});

View File

@ -0,0 +1,46 @@
import { extractDomainFromUrl } from 'discourse/lib/utilities';
import { h } from 'virtual-dom';
const _decorators = [];
export function addFeaturedLinkMetaDecorator(decorator) {
_decorators.push(decorator);
}
function extractLinkMeta(topic) {
const href = topic.featured_link, target = Discourse.SiteSettings.open_topic_featured_link_in_external_window ? '_blank' : '';
if (!href) { return; }
let domain = extractDomainFromUrl(href);
if (!domain) { return; }
// www appears frequently, so we truncate it
if (domain && domain.substr(0, 4) === 'www.') {
domain = domain.substring(4);
}
const meta = { target, href, domain, rel: 'nofollow' };
if (_decorators.length) {
_decorators.forEach(cb => cb(meta));
}
return meta;
}
export default function renderTopicFeaturedLink(topic) {
const meta = extractLinkMeta(topic);
if (meta) {
return `<a class="topic-featured-link" rel="${meta.rel}" target="${meta.target}" href="${meta.href}">${meta.domain}</a>`;
} else {
return '';
}
};
export function topicFeaturedLinkNode(topic) {
const meta = extractLinkMeta(topic);
if (meta) {
return h('a.topic-featured-link', {
attributes: { href: meta.href, rel: meta.rel, target: meta.target }
}, meta.domain);
}
}

View File

@ -169,6 +169,18 @@ const Category = RestModel.extend({
@computed("id") @computed("id")
isUncategorizedCategory(id) { isUncategorizedCategory(id) {
return id === Discourse.Site.currentProp("uncategorized_category_id"); return id === Discourse.Site.currentProp("uncategorized_category_id");
},
@computed('custom_fields.topic_featured_link_allowed')
topicFeaturedLinkAllowed: {
get(allowed) {
return allowed === "true";
},
set(value) {
value = value ? "true" : "false";
this.set("custom_fields.topic_featured_link_allowed", value);
return value;
}
} }
}); });

View File

@ -32,13 +32,15 @@ const CLOSED = 'closed',
target_usernames: 'targetUsernames', target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime', typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime', composer_open_duration_msecs: 'composerTime',
tags: 'tags' tags: 'tags',
featured_link: 'featuredLink'
}, },
_edit_topic_serializer = { _edit_topic_serializer = {
title: 'topic.title', title: 'topic.title',
categoryId: 'topic.category.id', categoryId: 'topic.category.id',
tags: 'topic.tags' tags: 'topic.tags',
featuredLink: 'topic.featured_link'
}; };
const Composer = RestModel.extend({ const Composer = RestModel.extend({
@ -136,6 +138,14 @@ const Composer = RestModel.extend({
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'), canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'), canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
@computed('canEditTitle', 'creatingPrivateMessage', 'categoryId')
canEditTopicFeaturedLink(canEditTitle, creatingPrivateMessage, categoryId) {
if (!this.siteSettings.topic_featured_link_enabled || !canEditTitle || creatingPrivateMessage) { return false; }
const categoryIds = this.site.get('topic_featured_link_allowed_category_ids');
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
},
// Determine the appropriate title for this action // Determine the appropriate title for this action
actionTitle: function() { actionTitle: function() {
const topic = this.get('topic'); const topic = this.get('topic');
@ -180,6 +190,10 @@ const Composer = RestModel.extend({
}.property('action', 'post', 'topic', 'topic.title'), }.property('action', 'post', 'topic', 'topic.title'),
@computed('canEditTopicFeaturedLink')
showComposerEditor(canEditTopicFeaturedLink) {
return canEditTopicFeaturedLink ? !this.siteSettings.topic_featured_link_onebox : true;
},
// whether to disable the post button // whether to disable the post button
cantSubmitPost: function() { cantSubmitPost: function() {
@ -269,11 +283,12 @@ const Composer = RestModel.extend({
} }
}.property('privateMessage'), }.property('privateMessage'),
missingReplyCharacters: function() { @computed('minimumPostLength', 'replyLength', 'canEditTopicFeaturedLink')
const postType = this.get('post.post_type'); missingReplyCharacters(minimumPostLength, replyLength, canEditTopicFeaturedLink) {
if (postType === this.site.get('post_types.small_action')) { return 0; } if (this.get('post.post_type') === this.site.get('post_types.small_action') ||
return this.get('minimumPostLength') - this.get('replyLength'); canEditTopicFeaturedLink && this.siteSettings.topic_featured_link_onebox) { return 0; }
}.property('minimumPostLength', 'replyLength'), return minimumPostLength - replyLength;
},
/** /**
Minimum number of characters for a post body to be valid. Minimum number of characters for a post body to be valid.
@ -492,6 +507,14 @@ const Composer = RestModel.extend({
save(opts) { save(opts) {
if (!this.get('cantSubmitPost')) { if (!this.get('cantSubmitPost')) {
// change category may result in some effect for topic featured link
if (this.get('canEditTopicFeaturedLink')) {
if (this.siteSettings.topic_featured_link_onebox) { this.set('reply', null); }
} else {
this.set('featuredLink', null);
}
return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts); return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts);
} }
}, },
@ -512,7 +535,8 @@ const Composer = RestModel.extend({
stagedPost: false, stagedPost: false,
typingTime: 0, typingTime: 0,
composerOpened: null, composerOpened: null,
composerTotalOpened: 0 composerTotalOpened: 0,
featuredLink: null
}); });
}, },

View File

@ -19,6 +19,17 @@
</label> </label>
</section> </section>
{{#if siteSettings.topic_featured_link_enabled}}
<section class='field'>
<div class="allowed-topic-featured-link-category">
<label class="checkbox-label">
{{input type="checkbox" checked=category.topicFeaturedLinkAllowed}}
{{i18n 'category.topic_featured_link_allowed'}}
</label>
</div>
</section>
{{/if}}
<section class="field"> <section class="field">
<label> <label>
{{i18n "category.sort_order"}} {{i18n "category.sort_order"}}

View File

@ -2,12 +2,16 @@
{{bound-category-link topic.category.parentCategory}} {{bound-category-link topic.category.parentCategory}}
{{/if}} {{/if}}
{{bound-category-link topic.category hideParent=true}} {{bound-category-link topic.category hideParent=true}}
{{#if siteSettings.tagging_enabled}} <div class="topic-header-extra">
<div class="list-tags"> {{#if siteSettings.tagging_enabled}}
{{#each topic.tags as |t|}} <div class="list-tags">
{{discourse-tag t}} {{#each topic.tags as |t|}}
{{/each}} {{discourse-tag t}}
</div> {{/each}}
{{/if}} </div>
{{/if}}
{{#if siteSettings.topic_featured_link_enabled}}
{{topic-featured-link topic}}
{{/if}}
</div>
{{plugin-outlet "topic-category"}} {{plugin-outlet "topic-category"}}

View File

@ -80,9 +80,13 @@
{{/if}} {{/if}}
{{render "additional-composer-buttons" model}} {{render "additional-composer-buttons" model}}
{{/if}} {{/if}}
{{#if model.canEditTopicFeaturedLink}}
<div class="topic-featured-link-input">
{{text-field tabindex="4" type="url" value=model.featuredLink id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
</div>
{{/if}}
</div> </div>
{{/if}} {{/if}}
{{plugin-outlet "composer-fields"}} {{plugin-outlet "composer-fields"}}
</div> </div>

View File

@ -7,6 +7,9 @@
<td class='main-link clearfix' colspan="{{titleColSpan}}"> <td class='main-link clearfix' colspan="{{titleColSpan}}">
{{raw "topic-status" topic=topic}} {{raw "topic-status" topic=topic}}
{{topic-link topic}} {{topic-link topic}}
{{#if topic.featured_link}}
{{topic-featured-link topic}}
{{/if}}
{{plugin-outlet "topic-list-after-title"}} {{plugin-outlet "topic-list-after-title"}}
{{#if showTopicPostBadges}} {{#if showTopicPostBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}

View File

@ -86,6 +86,13 @@
{{/each}} {{/each}}
</div> </div>
{{/if}} {{/if}}
{{#if model.featured_link_changes}}
<div class='row'>
{{model.featured_link_changes.previous}}
&rarr;
{{model.featured_link_changes.current}}
</div>
{{/if}}
{{plugin-outlet "post-revisions"}} {{plugin-outlet "post-revisions"}}

View File

@ -25,6 +25,9 @@
{{category-chooser valueAttribute="id" value=buffered.category_id}} {{category-chooser valueAttribute="id" value=buffered.category_id}}
{{/if}} {{/if}}
{{#if canEditTopicFeaturedLink}}
{{text-field type="url" value=buffered.featured_link id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
{{/if}}
{{#if canEditTags}} {{#if canEditTags}}
<br> <br>
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}} {{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}

View File

@ -4,6 +4,7 @@ import { iconNode } from 'discourse/helpers/fa-icon-node';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
import RawHtml from 'discourse/widgets/raw-html'; import RawHtml from 'discourse/widgets/raw-html';
import { tagNode } from 'discourse/lib/render-tag'; import { tagNode } from 'discourse/lib/render-tag';
import { topicFeaturedLinkNode } from 'discourse/lib/render-topic-featured-link';
export default createWidget('header-topic-info', { export default createWidget('header-topic-info', {
tagName: 'div.extra-info-wrapper', tagName: 'div.extra-info-wrapper',
@ -44,12 +45,19 @@ export default createWidget('header-topic-info', {
title.push(this.attach('category-link', { category })); title.push(this.attach('category-link', { category }));
} }
const extra = [];
if (this.siteSettings.tagging_enabled) { if (this.siteSettings.tagging_enabled) {
const tags = topic.get('tags') || []; const tags = topic.get('tags') || [];
if (tags.length) { if (tags.length) {
title.push(h('div.list-tags', tags.map(tagNode))); extra.push(h('div.list-tags', tags.map(tagNode)));
} }
} }
if (this.siteSettings.topic_featured_link_enabled) {
extra.push(topicFeaturedLinkNode(attrs.topic));
}
if (extra) {
title.push(h('div.topic-header-extra', extra));
}
} }
const contents = h('div.title-wrapper', title); const contents = h('div.title-wrapper', title);

View File

@ -187,6 +187,10 @@ div.ac-wrap {
} }
} }
#reply-control.topic-featured-link-only.open {
.wmd-controls { display: none; }
}
#cancel-file-upload { #cancel-file-upload {
font-size: 1.6em; font-size: 1.6em;
} }

View File

@ -27,18 +27,11 @@
} }
} }
.extra-info-wrapper { .topic-header-extra .discourse-tag {
.list-tags { -webkit-animation: fadein .7s;
padding-top: 5px; animation: fadein .7s;
}
.discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
} }
.add-tags .select2 { .add-tags .select2 {
margin: 0; margin: 0;
} }
@ -139,8 +132,8 @@ $tag-color: scale-color($primary, $lightness: 40%);
header .discourse-tag {color: $tag-color } header .discourse-tag {color: $tag-color }
.list-tags { .list-tags {
margin-right: 3px;
display: inline; display: inline;
margin: 0 0 0 5px;
font-size: 0.857em; font-size: 0.857em;
} }
@ -171,24 +164,6 @@ header .discourse-tag {color: $tag-color }
left: auto; left: auto;
} }
.bullet + .list-tags {
display: block;
line-height: 15px;
}
.bar + .list-tags {
line-height: 1.25;
.discourse-tag {
vertical-align: middle;
}
}
.box + .list-tags {
display: inline-block;
margin: 5px 0 0 5px;
padding-top: 2px;
}
.tag-sort-options { .tag-sort-options {
margin-bottom: 20px; margin-bottom: 20px;
a { a {

View File

@ -9,6 +9,10 @@
.badge-wrapper { .badge-wrapper {
float: left; float: left;
} }
a.topic-featured-link {
display: inline-block;
}
} }
a.badge-category { a.badge-category {
@ -47,7 +51,7 @@
display: inline; display: inline;
} }
#suggested-topics h3 .badge-wrapper.bullet span.badge-category, { #suggested-topics h3 .badge-wrapper.bullet span.badge-category {
// Override vertical-align: text-top from `badges.css.scss` // Override vertical-align: text-top from `badges.css.scss`
vertical-align: baseline; vertical-align: baseline;
line-height: 1.2; line-height: 1.2;
@ -133,3 +137,18 @@
} }
} }
} }
a.topic-featured-link {
display: inline-block;
text-transform: lowercase;
color: #858585;
font-size: 0.875rem;
&::before {
position: relative;
top: 0.1em;
padding-right: 3px;
font-family: FontAwesome;
content: "\f08e";
}
}

View File

@ -133,6 +133,10 @@
} }
} }
.extra-info-wrapper .title-wrapper .badge-wrapper.bar {
margin-top: 6px;
}
.autocomplete, td.category { .autocomplete, td.category {
.badge-wrapper { .badge-wrapper {
max-width: 230px; max-width: 230px;

View File

@ -298,6 +298,11 @@
background-color: dark-light-diff($primary, $secondary, 90%, -60%); background-color: dark-light-diff($primary, $secondary, 90%, -60%);
} }
} }
#topic-featured-link {
padding: 7px 10px;
margin: 6px 10px 3px 0;
width: 400px;
}
.d-editor-input:disabled { .d-editor-input:disabled {
background-color: dark-light-diff($primary, $secondary, 90%, -60%); background-color: dark-light-diff($primary, $secondary, 90%, -60%);
} }
@ -465,6 +470,10 @@
} }
} }
#reply-control.topic-featured-link-only.open {
height: 200px;
}
.control-row.reply-area { .control-row.reply-area {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;

View File

@ -505,13 +505,13 @@ video {
.extra-info-wrapper { .extra-info-wrapper {
overflow: hidden; overflow: hidden;
.badge-wrapper, i, .topic-link { .badge-wrapper, i, .topic-link {
-webkit-animation: fadein .7s; -webkit-animation: fadein .7s;
animation: fadein .7s; animation: fadein .7s;
} }
.topic-statuses { .topic-statuses {
i { color: $header_primary; } i { color: $header_primary; }
i.fa-envelope { color: $danger; } i.fa-envelope { color: $danger; }
.unpinned { color: $header_primary; } .unpinned { color: $header_primary; }
} }
@ -523,6 +523,26 @@ video {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.topic-header-extra {
margin: 0 0 0 5px;
padding-top: 5px;
}
}
.bullet + .topic-header-extra {
display: block;
line-height: 12px;
}
.bar + .topic-header-extra {
line-height: 1.25;
}
.box + .topic-header-extra {
display: inline-block;
margin: 0 0 0 5px;
padding-top: 5px;
} }
/* default docked header CSS for all topics, including those without categories */ /* default docked header CSS for all topics, including those without categories */

View File

@ -574,7 +574,6 @@ class PostsController < ApplicationController
end end
params.require(:raw)
result = params.permit(*permitted).tap do |whitelisted| result = params.permit(*permitted).tap do |whitelisted|
whitelisted[:image_sizes] = params[:image_sizes] whitelisted[:image_sizes] = params[:image_sizes]
# TODO this does not feel right, we should name what meta_data is allowed # TODO this does not feel right, we should name what meta_data is allowed

View File

@ -282,4 +282,15 @@ module ApplicationHelper
result.html_safe result.html_safe
end end
def topic_featured_link_domain(link)
begin
uri = URI.encode(link)
uri = URI.parse(uri)
uri = URI.parse("http://#{uri}") if uri.scheme.nil?
host = uri.host.downcase
host.start_with?('www.') ? host[4..-1] : host
rescue
''
end
end
end end

View File

@ -7,6 +7,7 @@ require_dependency 'text_cleaner'
require_dependency 'archetype' require_dependency 'archetype'
require_dependency 'html_prettify' require_dependency 'html_prettify'
require_dependency 'discourse_tagging' require_dependency 'discourse_tagging'
require_dependency 'discourse_featured_link'
class Topic < ActiveRecord::Base class Topic < ActiveRecord::Base
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
@ -73,6 +74,10 @@ class Topic < ActiveRecord::Base
(!t.user_id || !t.user.staff?) (!t.user_id || !t.user.staff?)
} }
validates :featured_link, allow_nil: true, format: URI::regexp(%w(http https))
validate if: :featured_link do
errors.add(:featured_link, :invalid_category) unless Guardian.new.can_edit_featured_link?(category_id)
end
before_validation do before_validation do
self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty? self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty?
@ -378,6 +383,14 @@ class Topic < ActiveRecord::Base
featured_topic_ids ? topics.where("topics.id NOT IN (?)", featured_topic_ids) : topics featured_topic_ids ? topics.where("topics.id NOT IN (?)", featured_topic_ids) : topics
end end
def featured_link
custom_fields[DiscourseFeaturedLink::CUSTOM_FIELD_NAME]
end
def featured_link=(link)
custom_fields[DiscourseFeaturedLink::CUSTOM_FIELD_NAME] = link.strip
end
def meta_data=(data) def meta_data=(data)
custom_fields.replace(data) custom_fields.replace(data)
end end

View File

@ -1,4 +1,5 @@
require_dependency 'avatar_lookup' require_dependency 'avatar_lookup'
require_dependency 'discourse_featured_link'
class TopicList class TopicList
include ActiveModel::Serialization include ActiveModel::Serialization
@ -27,6 +28,7 @@ class TopicList
end end
preloaded_custom_fields << DiscourseTagging::TAGS_FIELD_NAME if SiteSetting.tagging_enabled preloaded_custom_fields << DiscourseTagging::TAGS_FIELD_NAME if SiteSetting.tagging_enabled
preloaded_custom_fields << DiscourseFeaturedLink::CUSTOM_FIELD_NAME if SiteSetting.topic_featured_link_enabled
end end
def tags def tags

View File

@ -193,6 +193,10 @@ class PostRevisionSerializer < ApplicationSerializer
end end
end end
if SiteSetting.topic_featured_link_enabled
latest_modifications["featured_link"] = [post.topic.featured_link]
end
if SiteSetting.tagging_enabled if SiteSetting.tagging_enabled
latest_modifications["tags"] = [post.topic.tags.map(&:name)] latest_modifications["tags"] = [post.topic.tags.map(&:name)]
end end

View File

@ -23,7 +23,8 @@ class SiteSerializer < ApplicationSerializer
:can_tag_topics, :can_tag_topics,
:tags_filter_regexp, :tags_filter_regexp,
:top_tags, :top_tags,
:wizard_required :wizard_required,
:topic_featured_link_allowed_category_ids
has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :categories, serializer: BasicCategorySerializer, embed: :objects
has_many :trust_levels, embed: :objects has_many :trust_levels, embed: :objects
@ -121,4 +122,12 @@ class SiteSerializer < ApplicationSerializer
def include_wizard_required? def include_wizard_required?
Wizard.user_requires_completion?(scope.user) Wizard.user_requires_completion?(scope.user)
end end
def include_topic_featured_link_allowed_category_ids?
SiteSetting.topic_featured_link_enabled
end
def topic_featured_link_allowed_category_ids
scope.topic_featured_link_allowed_category_ids
end
end end

View File

@ -7,7 +7,7 @@ class SuggestedTopicSerializer < ListableTopicSerializer
has_one :user, serializer: BasicUserSerializer, embed: :objects has_one :user, serializer: BasicUserSerializer, embed: :objects
end end
attributes :archetype, :like_count, :views, :category_id, :tags attributes :archetype, :like_count, :views, :category_id, :tags, :featured_link
has_many :posters, serializer: SuggestedPosterSerializer, embed: :objects has_many :posters, serializer: SuggestedPosterSerializer, embed: :objects
def posters def posters
@ -21,4 +21,12 @@ class SuggestedTopicSerializer < ListableTopicSerializer
def tags def tags
object.tags.map(&:name) object.tags.map(&:name)
end end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def featured_link
object.featured_link
end
end end

View File

@ -10,7 +10,8 @@ class TopicListItemSerializer < ListableTopicSerializer
:pinned_globally, :pinned_globally,
:bookmarked_post_numbers, :bookmarked_post_numbers,
:liked_post_numbers, :liked_post_numbers,
:tags :tags,
:featured_link
has_many :posters, serializer: TopicPosterSerializer, embed: :objects has_many :posters, serializer: TopicPosterSerializer, embed: :objects
has_many :participants, serializer: TopicPosterSerializer, embed: :objects has_many :participants, serializer: TopicPosterSerializer, embed: :objects
@ -72,4 +73,12 @@ class TopicListItemSerializer < ListableTopicSerializer
object.tags.map(&:name) object.tags.map(&:name)
end end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def featured_link
object.featured_link
end
end end

View File

@ -56,7 +56,8 @@ class TopicViewSerializer < ApplicationSerializer
:chunk_size, :chunk_size,
:bookmarked, :bookmarked,
:message_archived, :message_archived,
:tags :tags,
:featured_link
# TODO: Split off into proper object / serializer # TODO: Split off into proper object / serializer
def details def details
@ -243,8 +244,17 @@ class TopicViewSerializer < ApplicationSerializer
def include_tags? def include_tags?
SiteSetting.tagging_enabled SiteSetting.tagging_enabled
end end
def tags def tags
object.topic.tags.map(&:name) object.topic.tags.map(&:name)
end end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def featured_link
object.topic.featured_link
end
end end

View File

@ -117,6 +117,9 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<strong><%= t.title -%></strong> <strong><%= t.title -%></strong>
</a> </a>
</h2> </h2>
<%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %>
<a class='topic-featured-link' href='<%= t.featured_link %>'><%= raw topic_featured_link_domain(t.featured_link) %></a>
<%- end %>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -328,6 +331,9 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<a href="<%= Discourse.base_url_no_prefix + t.relative_url %>" style="color:#2F70AC;font-weight:400;line-height:1.3;margin:0;padding:0;text-decoration:none"> <a href="<%= Discourse.base_url_no_prefix + t.relative_url %>" style="color:#2F70AC;font-weight:400;line-height:1.3;margin:0;padding:0;text-decoration:none">
<strong><%= t.title -%></strong> <strong><%= t.title -%></strong>
</a> </a>
<%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %>
<a class='topic-featured-link' href='<%= t.featured_link %>'><%= raw topic_featured_link_domain(t.featured_link) %></a>
<%- end %>
<p style="color:#0a0a0a;line-height:1.3;margin:0 0 10px 0;padding:0;text-align:left"> <p style="color:#0a0a0a;line-height:1.3;margin:0 0 10px 0;padding:0;text-align:left">
<%= category_badge(t.category, inline_style: true, absolute_url: true) %> <%= category_badge(t.category, inline_style: true, absolute_url: true) %>
</p> </p>

View File

@ -1074,6 +1074,7 @@ en:
title_placeholder: "What is this discussion about in one brief sentence?" title_placeholder: "What is this discussion about in one brief sentence?"
edit_reason_placeholder: "why are you editing?" edit_reason_placeholder: "why are you editing?"
show_edit_reason: "(add edit reason)" show_edit_reason: "(add edit reason)"
topic_featured_link_placeholder: "Enter link shown with title."
reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste images." reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste images."
view_new_post: "View your new post." view_new_post: "View your new post."
saving: "Saving" saving: "Saving"
@ -1875,6 +1876,7 @@ en:
tags_allowed_tag_groups: "Tag groups that can only be used in this category:" tags_allowed_tag_groups: "Tag groups that can only be used in this category:"
tags_placeholder: "(Optional) list of allowed tags" tags_placeholder: "(Optional) list of allowed tags"
tag_groups_placeholder: "(Optional) list of allowed tag groups" tag_groups_placeholder: "(Optional) list of allowed tag groups"
topic_featured_link_allowed: "Restricts editing the topic featured link in this category. Require site setting topic_featured_link_enabled is checked."
delete: 'Delete Category' delete: 'Delete Category'
create: 'New Category' create: 'New Category'
create_long: 'Create a new category' create_long: 'Create a new category'

View File

@ -320,6 +320,7 @@ en:
name: "Category Name" name: "Category Name"
topic: topic:
title: 'Title' title: 'Title'
featured_link: 'Featured Link'
post: post:
raw: "Body" raw: "Body"
user_profile: user_profile:
@ -336,6 +337,9 @@ en:
too_many_users: "You can only send warnings to one user at a time." too_many_users: "You can only send warnings to one user at a time."
cant_send_pm: "Sorry, you cannot send a private message to that user." cant_send_pm: "Sorry, you cannot send a private message to that user."
no_user_selected: "You must select a valid user." no_user_selected: "You must select a valid user."
featured_link:
invalid: "is invalid. URL should include http:// or https://."
invalid_category: "can't be edited in this category."
user: user:
attributes: attributes:
password: password:
@ -846,6 +850,10 @@ en:
min_first_post_length: "Minimum allowed first post (topic body) length in characters" min_first_post_length: "Minimum allowed first post (topic body) length in characters"
min_private_message_post_length: "Minimum allowed post length in characters for messages" min_private_message_post_length: "Minimum allowed post length in characters for messages"
max_post_length: "Maximum allowed post length in characters" max_post_length: "Maximum allowed post length in characters"
topic_featured_link_enabled: "Enable posting a link with topics."
topic_featured_link_onebox: "Show an onebox in the post body if possible and prevent editing post content."
open_topic_featured_link_in_external_window: "Open topic featured link in a external window."
show_topic_featured_link_in_digest: "Show the topic featured link in the digest email."
min_topic_title_length: "Minimum allowed topic title length in characters" min_topic_title_length: "Minimum allowed topic title length in characters"
max_topic_title_length: "Maximum allowed topic title length in characters" max_topic_title_length: "Maximum allowed topic title length in characters"
min_private_message_title_length: "Minimum allowed title length for a message in characters" min_private_message_title_length: "Minimum allowed title length for a message in characters"

View File

@ -433,6 +433,15 @@ posting:
max_post_length: max_post_length:
client: true client: true
default: 32000 default: 32000
topic_featured_link_enabled:
client: true
default: false
topic_featured_link_onebox:
client: true
default: false
open_topic_featured_link_in_external_window:
client: true
default: true
body_min_entropy: 7 body_min_entropy: 7
min_topic_title_length: min_topic_title_length:
client: true client: true
@ -596,6 +605,7 @@ email:
disable_digest_emails: disable_digest_emails:
default: false default: false
client: true client: true
show_topic_featured_link_in_digest: true
email_custom_headers: 'Auto-Submitted: auto-generated' email_custom_headers: 'Auto-Submitted: auto-generated'
email_subject: '[%{site_name}] %{optional_pm}%{optional_cat}%{topic_title}' email_subject: '[%{site_name}] %{optional_pm}%{optional_cat}%{topic_title}'
reply_by_email_enabled: reply_by_email_enabled:

View File

@ -0,0 +1,27 @@
module DiscourseFeaturedLink
CUSTOM_FIELD_NAME = 'featured_link'.freeze
AdminDashboardData::GLOBAL_REPORTS << CUSTOM_FIELD_NAME
Report.add_report(CUSTOM_FIELD_NAME) do |report|
report.data = []
link_topics = TopicCustomField.where(name: CUSTOM_FIELD_NAME)
link_topics = link_topics.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
link_topics.where("topic_custom_fields.created_at >= ?", report.start_date)
.where("topic_custom_fields.created_at <= ?", report.end_date)
.group("DATE(topic_custom_fields.created_at)")
.order("DATE(topic_custom_fields.created_at)")
.count
.each { |date, count| report.data << { x: date, y: count } }
report.total = link_topics.count
report.prev30Days = link_topics.where("topic_custom_fields.created_at >= ?", report.start_date - 30.days)
.where("topic_custom_fields.created_at <= ?", report.start_date)
.count
end
def self.cache_onebox_link(link)
# If the link is pasted swiftly, onebox may not have time to cache it
Oneboxer.onebox(link, invalidate_oneboxes: false)
link
end
end

View File

@ -67,6 +67,11 @@ module Email
add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/ add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/
end end
# topic featured link
@fragment.css('a.topic-featured-link').each do |e|
e['style'] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);"
end
# attachments # attachments
@fragment.css('a.attachment').each do |a| @fragment.css('a.attachment').each do |a|
# ensure all urls are absolute # ensure all urls are absolute

View File

@ -68,4 +68,9 @@ module CategoryGuardian
def topic_create_allowed_category_ids def topic_create_allowed_category_ids
@topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids @topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids
end end
def topic_featured_link_allowed_category_ids
@topic_featured_link_allowed_category_ids = CategoryCustomField.where(name: "topic_featured_link_allowed", value: "true")
.pluck(:category_id)
end
end end

View File

@ -105,4 +105,9 @@ module TopicGuardian
records records
end end
def can_edit_featured_link?(category_id)
SiteSetting.topic_featured_link_enabled &&
(topic_featured_link_allowed_category_ids.empty? || # no per category restrictions
category_id && topic_featured_link_allowed_category_ids.include?(category_id.to_i)) # category restriction exists
end
end end

View File

@ -5,6 +5,7 @@ require_dependency 'topic_creator'
require_dependency 'post_jobs_enqueuer' require_dependency 'post_jobs_enqueuer'
require_dependency 'distributed_mutex' require_dependency 'distributed_mutex'
require_dependency 'has_errors' require_dependency 'has_errors'
require_dependency 'discourse_featured_link'
class PostCreator class PostCreator
include HasErrors include HasErrors
@ -103,6 +104,11 @@ class PostCreator
end end
end end
onebox_featured_link = SiteSetting.topic_featured_link_enabled && SiteSetting.topic_featured_link_onebox && guardian.can_edit_featured_link?(find_category_id)
if onebox_featured_link
@opts[:raw] = DiscourseFeaturedLink.cache_onebox_link(@opts[:featured_link])
end
setup_post setup_post
return true if skip_validations? return true if skip_validations?
@ -116,7 +122,7 @@ class PostCreator
DiscourseEvent.trigger :before_create_post, @post DiscourseEvent.trigger :before_create_post, @post
DiscourseEvent.trigger :validate_post, @post DiscourseEvent.trigger :validate_post, @post
post_validator = Validators::PostValidator.new(skip_topic: true) post_validator = Validators::PostValidator.new(skip_topic: true, skip_post_body: onebox_featured_link)
post_validator.validate(@post) post_validator.validate(@post)
valid = @post.errors.blank? valid = @post.errors.blank?
@ -338,6 +344,18 @@ class PostCreator
private private
# TODO: merge the similar function in TopicCreator and fix parameter naming for `category`
def find_category_id
@opts.delete(:category) if @opts[:archetype].present? && @opts[:archetype] == Archetype.private_message
category = if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /^\d+$/)
Category.find_by(id: @opts[:category])
else
Category.find_by(name_lower: @opts[:category].try(:downcase))
end
category&.id
end
def create_topic def create_topic
return if @topic return if @topic
begin begin

View File

@ -95,6 +95,23 @@ class PostRevisor
end end
end end
track_topic_field(:featured_link) do |topic_changes, featured_link|
if SiteSetting.topic_featured_link_enabled &&
featured_link.present? &&
topic_changes.guardian.can_edit_featured_link?(topic_changes.topic.category_id)
topic_changes.record_change('featured_link', topic_changes.topic.featured_link, featured_link)
topic_changes.topic.featured_link = featured_link
if SiteSetting.topic_featured_link_onebox
post = topic_changes.topic.first_post
post.raw = DiscourseFeaturedLink.cache_onebox_link(featured_link)
post.save!
post.rebake!
end
end
end
# AVAILABLE OPTIONS: # AVAILABLE OPTIONS:
# - revised_at: changes the date of the revision # - revised_at: changes the date of the revision
# - force_new_version: bypass ninja-edit window # - force_new_version: bypass ninja-edit window

View File

@ -124,6 +124,10 @@ class TopicCreator
topic_params[:pinned_at] = Time.zone.parse(@opts[:pinned_at].to_s) if @opts[:pinned_at].present? topic_params[:pinned_at] = Time.zone.parse(@opts[:pinned_at].to_s) if @opts[:pinned_at].present?
topic_params[:pinned_globally] = @opts[:pinned_globally] if @opts[:pinned_globally].present? topic_params[:pinned_globally] = @opts[:pinned_globally] if @opts[:pinned_globally].present?
if SiteSetting.topic_featured_link_enabled && @opts[:featured_link].present? && @guardian.can_edit_featured_link?(topic_params[:category_id])
topic_params[:featured_link] = @opts[:featured_link]
end
topic_params topic_params
end end

View File

@ -10,8 +10,7 @@ class Validators::PostValidator < ActiveModel::Validator
return if record.acting_user.try(:staged?) return if record.acting_user.try(:staged?)
return if record.acting_user.try(:admin?) && Discourse.static_doc_topic_ids.include?(record.topic_id) return if record.acting_user.try(:admin?) && Discourse.static_doc_topic_ids.include?(record.topic_id)
stripped_length(record) post_body_validator(record)
raw_quality(record)
max_posts_validator(record) max_posts_validator(record)
max_mention_validator(record) max_mention_validator(record)
max_images_validator(record) max_images_validator(record)
@ -21,8 +20,6 @@ class Validators::PostValidator < ActiveModel::Validator
end end
def presence(post) def presence(post)
post.errors.add(:raw, :blank, options) if post.raw.blank?
unless options[:skip_topic] unless options[:skip_topic]
post.errors.add(:topic_id, :blank, options) if post.topic_id.blank? post.errors.add(:topic_id, :blank, options) if post.topic_id.blank?
end end
@ -32,6 +29,12 @@ class Validators::PostValidator < ActiveModel::Validator
end end
end end
def post_body_validator(post)
return if options[:skip_post_body]
stripped_length(post)
raw_quality(post)
end
def stripped_length(post) def stripped_length(post)
range = if private_message?(post) range = if private_message?(post)
# private message # private message

View File

@ -2280,4 +2280,27 @@ describe Guardian do
end end
end end
end end
context 'topic featured link category restriction' do
before { SiteSetting.topic_featured_link_enabled = true }
let(:guardian) { Guardian.new }
it 'returns true if no category restricts editing link' do
expect(guardian.can_edit_featured_link?(nil)).to eq(true)
expect(guardian.can_edit_featured_link?(5)).to eq(true)
end
context 'when exist' do
let!(:category) { Fabricate(:category) }
let!(:link_category) { Fabricate(:link_category) }
it 'returns true if the category is listed' do
expect(guardian.can_edit_featured_link?(link_category.id)).to eq(true)
end
it 'returns false if the category is not listed' do
expect(guardian.can_edit_featured_link?(category.id)).to eq(false)
end
end
end
end end

View File

@ -20,6 +20,7 @@ describe PostCreator do
let(:creator_with_category) { PostCreator.new(user, basic_topic_params.merge(category: category.id )) } let(:creator_with_category) { PostCreator.new(user, basic_topic_params.merge(category: category.id )) }
let(:creator_with_meta_data) { PostCreator.new(user, basic_topic_params.merge(meta_data: {hello: "world"} )) } let(:creator_with_meta_data) { PostCreator.new(user, basic_topic_params.merge(meta_data: {hello: "world"} )) }
let(:creator_with_image_sizes) { PostCreator.new(user, basic_topic_params.merge(image_sizes: image_sizes)) } let(:creator_with_image_sizes) { PostCreator.new(user, basic_topic_params.merge(image_sizes: image_sizes)) }
let(:creator_with_featured_link) { PostCreator.new(user, title: "featured link topic", archetype_id: 1, featured_link: "http://discourse.org") }
it "can create a topic with null byte central" do it "can create a topic with null byte central" do
post = PostCreator.create(user, title: "hello\u0000world this is title", raw: "this is my\u0000 first topic") post = PostCreator.create(user, title: "hello\u0000world this is title", raw: "this is my\u0000 first topic")
@ -243,6 +244,14 @@ describe PostCreator do
end end
end end
it 'creates a post without raw' do
SiteSetting.topic_featured_link_enabled = true
SiteSetting.topic_featured_link_onebox = true
post = creator_with_featured_link.create
expect(post.topic.featured_link).to eq('http://discourse.org')
expect(post.raw).to eq('http://discourse.org')
end
describe "topic's auto close" do describe "topic's auto close" do
it "doesn't update topic's auto close when it's not based on last post" do it "doesn't update topic's auto close when it's not based on last post" do

View File

@ -5,6 +5,16 @@ describe Validators::PostValidator do
let(:post) { build(:post) } let(:post) { build(:post) }
let(:validator) { Validators::PostValidator.new({}) } let(:validator) { Validators::PostValidator.new({}) }
context "when empty raw can bypass post body validation" do
let(:validator) { Validators::PostValidator.new(skip_post_body: true) }
it "should be allowed for empty raw based on site setting" do
post.raw = ""
validator.post_body_validator(post)
expect(post.errors).to be_empty
end
end
context "stripped_length" do context "stripped_length" do
it "adds an error for short raw" do it "adds an error for short raw" do
post.raw = "abc" post.raw = "abc"

View File

@ -579,10 +579,6 @@ describe PostsController do
let(:moderator) { log_in(:moderator) } let(:moderator) { log_in(:moderator) }
let(:new_post) { Fabricate.build(:post, user: user) } let(:new_post) { Fabricate.build(:post, user: user) }
it "raises an exception without a raw parameter" do
expect { xhr :post, :create }.to raise_error(ActionController::ParameterMissing)
end
context "fast typing" do context "fast typing" do
before do before do
SiteSetting.min_first_post_typing_time = 3000 SiteSetting.min_first_post_typing_time = 3000
@ -771,8 +767,8 @@ describe PostsController do
end end
it "passes category through" do it "passes category through" do
xhr :post, :create, {raw: 'hello', category: 'cool'} xhr :post, :create, {raw: 'hello', category: 1}
expect(assigns(:manager_params)['category']).to eq('cool') expect(assigns(:manager_params)['category']).to eq('1')
end end
it "passes target_usernames through" do it "passes target_usernames through" do

View File

@ -25,3 +25,7 @@ Fabricator(:private_category, from: :category) do
cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full]) cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full])
end end
end end
Fabricator(:link_category, from: :category) do
before_validation { |category, transients| category.custom_fields['topic_featured_link_allowed'] = 'true' }
end

View File

@ -421,14 +421,14 @@ describe Category do
describe 'latest' do describe 'latest' do
it 'should be updated correctly' do it 'should be updated correctly' do
category = Fabricate(:category) category = Fabricate(:category)
post = create_post(category: category.name) post = create_post(category: category.id)
category.reload category.reload
expect(category.latest_post_id).to eq(post.id) expect(category.latest_post_id).to eq(post.id)
expect(category.latest_topic_id).to eq(post.topic_id) expect(category.latest_topic_id).to eq(post.topic_id)
post2 = create_post(category: category.name) post2 = create_post(category: category.id)
post3 = create_post(topic_id: post.topic_id, category: category.name) post3 = create_post(topic_id: post.topic_id, category: category.id)
category.reload category.reload
expect(category.latest_post_id).to eq(post3.id) expect(category.latest_post_id).to eq(post3.id)
@ -451,7 +451,7 @@ describe Category do
context 'with regular topics' do context 'with regular topics' do
before do before do
create_post(user: @category.user, category: @category.name) create_post(user: @category.user, category: @category.id)
Category.update_stats Category.update_stats
@category.reload @category.reload
end end
@ -491,7 +491,7 @@ describe Category do
context 'with revised post' do context 'with revised post' do
before do before do
post = create_post(user: @category.user, category: @category.name) post = create_post(user: @category.user, category: @category.id)
SiteSetting.stubs(:editing_grace_period).returns(1.minute.to_i) SiteSetting.stubs(:editing_grace_period).returns(1.minute.to_i)
post.revise(post.user, { raw: 'updated body' }, revised_at: post.updated_at + 2.minutes) post.revise(post.user, { raw: 'updated body' }, revised_at: post.updated_at + 2.minutes)

View File

@ -1725,7 +1725,6 @@ describe Topic do
expect(@topic_status_event_triggered).to eq(true) expect(@topic_status_event_triggered).to eq(true)
end end
it 'allows users to normalize counts' do it 'allows users to normalize counts' do
topic = Fabricate(:topic, last_posted_at: 1.year.ago) topic = Fabricate(:topic, last_posted_at: 1.year.ago)
@ -1741,4 +1740,39 @@ describe Topic do
expect(topic.last_posted_at).to be_within(1.second).of (post1.created_at) expect(topic.last_posted_at).to be_within(1.second).of (post1.created_at)
end end
context 'featured link' do
before { SiteSetting.topic_featured_link_enabled = true }
let(:topic) { Fabricate(:topic) }
it 'can validate featured link' do
topic.featured_link = ' invalid string'
expect(topic).not_to be_valid
expect(topic.errors[:featured_link]).to be_present
end
it 'can properly save the featured link' do
topic.featured_link = ' https://github.com/discourse/discourse'
expect(topic.save).to be_truthy
expect(topic.custom_fields['featured_link']).to eq('https://github.com/discourse/discourse')
end
context 'when category restricts present' do
let!(:link_category) { Fabricate(:link_category) }
let(:topic) { Fabricate(:topic) }
let(:link_topic) { Fabricate(:topic, category: link_category) }
it 'can save the featured link if it belongs to that category' do
link_topic.featured_link = 'https://github.com/discourse/discourse'
expect(link_topic.save).to be_truthy
expect(link_topic.custom_fields['featured_link']).to eq('https://github.com/discourse/discourse')
end
it 'can not save the featured link if it belongs to that category' do
topic.featured_link = 'https://github.com/discourse/discourse'
expect(topic.save).to be_falsey
end
end
end
end end

View File

@ -28,7 +28,7 @@ module Helpers
args[:title] ||= "This is my title #{Helpers.next_seq}" args[:title] ||= "This is my title #{Helpers.next_seq}"
user = args.delete(:user) || Fabricate(:user) user = args.delete(:user) || Fabricate(:user)
guardian = Guardian.new(user) guardian = Guardian.new(user)
args[:category] = args[:category].name if args[:category].is_a?(Category) args[:category] = args[:category].id if args[:category].is_a?(Category)
TopicCreator.create(user, guardian, args) TopicCreator.create(user, guardian, args)
end end
@ -37,7 +37,7 @@ module Helpers
args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}" args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
args[:topic_id] = args[:topic].id if args[:topic] args[:topic_id] = args[:topic].id if args[:topic]
user = args.delete(:user) || Fabricate(:user) user = args.delete(:user) || Fabricate(:user)
args[:category] = args[:category].name if args[:category].is_a?(Category) args[:category] = args[:category].id if args[:category].is_a?(Category)
creator = PostCreator.new(user, args) creator = PostCreator.new(user, args)
post = creator.create post = creator.create

View File

@ -40,6 +40,10 @@ test('missingReplyCharacters', function() {
missingReplyCharacters('hi', false, false, Discourse.SiteSettings.min_post_length - 2, 'too short public post'); missingReplyCharacters('hi', false, false, Discourse.SiteSettings.min_post_length - 2, 'too short public post');
missingReplyCharacters('hi', false, true, Discourse.SiteSettings.min_first_post_length - 2, 'too short first post'); missingReplyCharacters('hi', false, true, Discourse.SiteSettings.min_first_post_length - 2, 'too short first post');
missingReplyCharacters('hi', true, false, Discourse.SiteSettings.min_private_message_post_length - 2, 'too short private message'); missingReplyCharacters('hi', true, false, Discourse.SiteSettings.min_private_message_post_length - 2, 'too short private message');
Discourse.SiteSettings.topic_featured_link_onebox = true;
const composer = createComposer({ canEditTopicFeaturedLink: true });
equal(composer.get('missingReplyCharacters'), 0, "don't require any post content");
}); });
test('missingTitleCharacters', function() { test('missingTitleCharacters', function() {
@ -105,7 +109,7 @@ test("prependText", function() {
composer.prependText("world "); composer.prependText("world ");
equal(composer.get('reply'), "world hello", "it prepends text to existing text"); equal(composer.get('reply'), "world hello", "it prepends text to existing text");
composer.prependText("before new line", {new_line: true}); composer.prependText("before new line", {new_line: true});
equal(composer.get('reply'), "before new line\n\nworld hello", "it prepends text with new line to existing text"); equal(composer.get('reply'), "before new line\n\nworld hello", "it prepends text with new line to existing text");
}); });