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.createdPost:created-post',
'composer.creatingTopic:topic',
'composer.whisper:composing-whisper'],
'composer.whisper:composing-whisper',
'composer.showComposerEditor::topic-featured-link-only'],
@computed('composer.composeState')
composeState(composeState) {
@ -27,7 +28,7 @@ export default Ember.Component.extend({
this.appEvents.trigger("composer:resized");
},
@observes('composeState', 'composer.action')
@observes('composeState', 'composer.action', 'composer.canEditTopicFeaturedLink')
resize() {
Ember.run.scheduleOnce('afterRender', () => {
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"); }
}.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'),
currentTagChanges: customTagArray('model.tags_changes.current'),

View File

@ -160,6 +160,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return post => this.postSelected(post);
}.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')
canEditTags(isPrivateMessage) {
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")
isUncategorizedCategory(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',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime',
tags: 'tags'
tags: 'tags',
featured_link: 'featuredLink'
},
_edit_topic_serializer = {
title: 'topic.title',
categoryId: 'topic.category.id',
tags: 'topic.tags'
tags: 'topic.tags',
featuredLink: 'topic.featured_link'
};
const Composer = RestModel.extend({
@ -136,6 +138,14 @@ const Composer = RestModel.extend({
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
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
actionTitle: function() {
const topic = this.get('topic');
@ -180,6 +190,10 @@ const Composer = RestModel.extend({
}.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
cantSubmitPost: function() {
@ -269,11 +283,12 @@ const Composer = RestModel.extend({
}
}.property('privateMessage'),
missingReplyCharacters: function() {
const postType = this.get('post.post_type');
if (postType === this.site.get('post_types.small_action')) { return 0; }
return this.get('minimumPostLength') - this.get('replyLength');
}.property('minimumPostLength', 'replyLength'),
@computed('minimumPostLength', 'replyLength', 'canEditTopicFeaturedLink')
missingReplyCharacters(minimumPostLength, replyLength, canEditTopicFeaturedLink) {
if (this.get('post.post_type') === this.site.get('post_types.small_action') ||
canEditTopicFeaturedLink && this.siteSettings.topic_featured_link_onebox) { return 0; }
return minimumPostLength - replyLength;
},
/**
Minimum number of characters for a post body to be valid.
@ -492,6 +507,14 @@ const Composer = RestModel.extend({
save(opts) {
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);
}
},
@ -512,7 +535,8 @@ const Composer = RestModel.extend({
stagedPost: false,
typingTime: 0,
composerOpened: null,
composerTotalOpened: 0
composerTotalOpened: 0,
featuredLink: null
});
},

View File

@ -19,6 +19,17 @@
</label>
</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">
<label>
{{i18n "category.sort_order"}}

View File

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

View File

@ -80,9 +80,13 @@
{{/if}}
{{render "additional-composer-buttons" model}}
{{/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>
{{/if}}
{{plugin-outlet "composer-fields"}}
</div>

View File

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

View File

@ -86,6 +86,13 @@
{{/each}}
</div>
{{/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"}}

View File

@ -25,6 +25,9 @@
{{category-chooser valueAttribute="id" value=buffered.category_id}}
{{/if}}
{{#if canEditTopicFeaturedLink}}
{{text-field type="url" value=buffered.featured_link id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
{{/if}}
{{#if canEditTags}}
<br>
{{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 RawHtml from 'discourse/widgets/raw-html';
import { tagNode } from 'discourse/lib/render-tag';
import { topicFeaturedLinkNode } from 'discourse/lib/render-topic-featured-link';
export default createWidget('header-topic-info', {
tagName: 'div.extra-info-wrapper',
@ -44,12 +45,19 @@ export default createWidget('header-topic-info', {
title.push(this.attach('category-link', { category }));
}
const extra = [];
if (this.siteSettings.tagging_enabled) {
const tags = topic.get('tags') || [];
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);

View File

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

View File

@ -27,18 +27,11 @@
}
}
.extra-info-wrapper {
.list-tags {
padding-top: 5px;
}
.discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.topic-header-extra .discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.add-tags .select2 {
margin: 0;
}
@ -139,8 +132,8 @@ $tag-color: scale-color($primary, $lightness: 40%);
header .discourse-tag {color: $tag-color }
.list-tags {
margin-right: 3px;
display: inline;
margin: 0 0 0 5px;
font-size: 0.857em;
}
@ -171,24 +164,6 @@ header .discourse-tag {color: $tag-color }
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 {
margin-bottom: 20px;
a {

View File

@ -9,6 +9,10 @@
.badge-wrapper {
float: left;
}
a.topic-featured-link {
display: inline-block;
}
}
a.badge-category {
@ -47,7 +51,7 @@
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`
vertical-align: baseline;
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 {
.badge-wrapper {
max-width: 230px;

View File

@ -298,6 +298,11 @@
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 {
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 {
padding-left: 20px;
padding-right: 20px;

View File

@ -505,13 +505,13 @@ video {
.extra-info-wrapper {
overflow: hidden;
.badge-wrapper, i, .topic-link {
.badge-wrapper, i, .topic-link {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.topic-statuses {
i { color: $header_primary; }
i { color: $header_primary; }
i.fa-envelope { color: $danger; }
.unpinned { color: $header_primary; }
}
@ -523,6 +523,26 @@ video {
overflow: hidden;
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 */

View File

@ -574,7 +574,6 @@ class PostsController < ApplicationController
end
params.require(:raw)
result = params.permit(*permitted).tap do |whitelisted|
whitelisted[:image_sizes] = params[:image_sizes]
# 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
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

View File

@ -7,6 +7,7 @@ require_dependency 'text_cleaner'
require_dependency 'archetype'
require_dependency 'html_prettify'
require_dependency 'discourse_tagging'
require_dependency 'discourse_featured_link'
class Topic < ActiveRecord::Base
include ActionView::Helpers::SanitizeHelper
@ -73,6 +74,10 @@ class Topic < ActiveRecord::Base
(!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
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
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)
custom_fields.replace(data)
end

View File

@ -1,4 +1,5 @@
require_dependency 'avatar_lookup'
require_dependency 'discourse_featured_link'
class TopicList
include ActiveModel::Serialization
@ -27,6 +28,7 @@ class TopicList
end
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
def tags

View File

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

View File

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

View File

@ -7,7 +7,7 @@ class SuggestedTopicSerializer < ListableTopicSerializer
has_one :user, serializer: BasicUserSerializer, embed: :objects
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
def posters
@ -21,4 +21,12 @@ class SuggestedTopicSerializer < ListableTopicSerializer
def tags
object.tags.map(&:name)
end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def featured_link
object.featured_link
end
end

View File

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

View File

@ -56,7 +56,8 @@ class TopicViewSerializer < ApplicationSerializer
:chunk_size,
:bookmarked,
:message_archived,
:tags
:tags,
:featured_link
# TODO: Split off into proper object / serializer
def details
@ -243,8 +244,17 @@ class TopicViewSerializer < ApplicationSerializer
def include_tags?
SiteSetting.tagging_enabled
end
def tags
object.topic.tags.map(&:name)
end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def featured_link
object.topic.featured_link
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>
</a>
</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>
</tr>
</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">
<strong><%= t.title -%></strong>
</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">
<%= category_badge(t.category, inline_style: true, absolute_url: true) %>
</p>

View File

@ -1074,6 +1074,7 @@ en:
title_placeholder: "What is this discussion about in one brief sentence?"
edit_reason_placeholder: "why are you editing?"
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."
view_new_post: "View your new post."
saving: "Saving"
@ -1875,6 +1876,7 @@ en:
tags_allowed_tag_groups: "Tag groups that can only be used in this category:"
tags_placeholder: "(Optional) list of allowed tags"
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'
create: 'New Category'
create_long: 'Create a new category'

View File

@ -320,6 +320,7 @@ en:
name: "Category Name"
topic:
title: 'Title'
featured_link: 'Featured Link'
post:
raw: "Body"
user_profile:
@ -336,6 +337,9 @@ en:
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."
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:
attributes:
password:
@ -846,6 +850,10 @@ en:
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"
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"
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"

View File

@ -433,6 +433,15 @@ posting:
max_post_length:
client: true
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
min_topic_title_length:
client: true
@ -596,6 +605,7 @@ email:
disable_digest_emails:
default: false
client: true
show_topic_featured_link_in_digest: true
email_custom_headers: 'Auto-Submitted: auto-generated'
email_subject: '[%{site_name}] %{optional_pm}%{optional_cat}%{topic_title}'
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/
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
@fragment.css('a.attachment').each do |a|
# ensure all urls are absolute

View File

@ -68,4 +68,9 @@ module CategoryGuardian
def topic_create_allowed_category_ids
@topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids
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

View File

@ -105,4 +105,9 @@ module TopicGuardian
records
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

View File

@ -5,6 +5,7 @@ require_dependency 'topic_creator'
require_dependency 'post_jobs_enqueuer'
require_dependency 'distributed_mutex'
require_dependency 'has_errors'
require_dependency 'discourse_featured_link'
class PostCreator
include HasErrors
@ -103,6 +104,11 @@ class PostCreator
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
return true if skip_validations?
@ -116,7 +122,7 @@ class PostCreator
DiscourseEvent.trigger :before_create_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)
valid = @post.errors.blank?
@ -338,6 +344,18 @@ class PostCreator
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
return if @topic
begin

View File

@ -95,6 +95,23 @@ class PostRevisor
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:
# - revised_at: changes the date of the revision
# - 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_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
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(:admin?) && Discourse.static_doc_topic_ids.include?(record.topic_id)
stripped_length(record)
raw_quality(record)
post_body_validator(record)
max_posts_validator(record)
max_mention_validator(record)
max_images_validator(record)
@ -21,8 +20,6 @@ class Validators::PostValidator < ActiveModel::Validator
end
def presence(post)
post.errors.add(:raw, :blank, options) if post.raw.blank?
unless options[:skip_topic]
post.errors.add(:topic_id, :blank, options) if post.topic_id.blank?
end
@ -32,6 +29,12 @@ class Validators::PostValidator < ActiveModel::Validator
end
end
def post_body_validator(post)
return if options[:skip_post_body]
stripped_length(post)
raw_quality(post)
end
def stripped_length(post)
range = if private_message?(post)
# private message

View File

@ -2280,4 +2280,27 @@ describe Guardian do
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

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_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_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
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
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
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(: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
it "adds an error for short raw" do
post.raw = "abc"

View File

@ -579,10 +579,6 @@ describe PostsController do
let(:moderator) { log_in(:moderator) }
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
before do
SiteSetting.min_first_post_typing_time = 3000
@ -771,8 +767,8 @@ describe PostsController do
end
it "passes category through" do
xhr :post, :create, {raw: 'hello', category: 'cool'}
expect(assigns(:manager_params)['category']).to eq('cool')
xhr :post, :create, {raw: 'hello', category: 1}
expect(assigns(:manager_params)['category']).to eq('1')
end
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])
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
it 'should be updated correctly' do
category = Fabricate(:category)
post = create_post(category: category.name)
post = create_post(category: category.id)
category.reload
expect(category.latest_post_id).to eq(post.id)
expect(category.latest_topic_id).to eq(post.topic_id)
post2 = create_post(category: category.name)
post3 = create_post(topic_id: post.topic_id, category: category.name)
post2 = create_post(category: category.id)
post3 = create_post(topic_id: post.topic_id, category: category.id)
category.reload
expect(category.latest_post_id).to eq(post3.id)
@ -451,7 +451,7 @@ describe Category do
context 'with regular topics' do
before do
create_post(user: @category.user, category: @category.name)
create_post(user: @category.user, category: @category.id)
Category.update_stats
@category.reload
end
@ -491,7 +491,7 @@ describe Category do
context 'with revised post' 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)
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)
end
it 'allows users to normalize counts' do
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)
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

View File

@ -28,7 +28,7 @@ module Helpers
args[:title] ||= "This is my title #{Helpers.next_seq}"
user = args.delete(:user) || Fabricate(: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)
end
@ -37,7 +37,7 @@ module Helpers
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]
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)
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, 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');
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() {