mirror of
https://github.com/discourse/discourse.git
synced 2025-05-25 00:32:52 +08:00
FEATURE: Include a user's pending posts in the topic view
Also includes a refactor to TopicView's serializer which was not building our attributes using serializers properly.
This commit is contained in:
@ -679,6 +679,13 @@ export default Ember.Controller.extend({
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.responseJson.action === "enqueued") {
|
if (result.responseJson.action === "enqueued") {
|
||||||
this.send("postWasEnqueued", result.responseJson);
|
this.send("postWasEnqueued", result.responseJson);
|
||||||
|
if (result.responseJson.pending_post) {
|
||||||
|
let pendingPosts = this.get("topicController.model.pending_posts");
|
||||||
|
if (pendingPosts) {
|
||||||
|
pendingPosts.pushObject(result.responseJson.pending_post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.destroyDraft();
|
this.destroyDraft();
|
||||||
this.close();
|
this.close();
|
||||||
this.appEvents.trigger("post-stream:refresh");
|
this.appEvents.trigger("post-stream:refresh");
|
||||||
|
@ -202,6 +202,14 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
deletePending(pending) {
|
||||||
|
return ajax(`/review/${pending.id}`, { type: "DELETE" })
|
||||||
|
.then(() => {
|
||||||
|
this.get("model.pending_posts").removeObject(pending);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
showPostFlags(post) {
|
showPostFlags(post) {
|
||||||
return this.send("showFlags", post);
|
return this.send("showFlags", post);
|
||||||
},
|
},
|
||||||
|
@ -211,10 +211,41 @@
|
|||||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||||
{{#if loadedAllPosts}}
|
{{#if loadedAllPosts}}
|
||||||
|
|
||||||
{{#if model.pending_posts_count}}
|
{{#if model.pending_posts}}
|
||||||
|
<div class='pending-posts'>
|
||||||
|
{{#each model.pending_posts as |pending|}}
|
||||||
|
<div class='reviewable-item'>
|
||||||
|
<div class='reviewable-meta-data'>
|
||||||
|
<span class='reviewable-type'>
|
||||||
|
{{i18n "review.awaiting_approval"}}
|
||||||
|
</span>
|
||||||
|
<span class='created-at'>
|
||||||
|
{{age-with-tooltip pending.created_at}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class='post-contents-wrapper'>
|
||||||
|
{{reviewable-created-by user=currentUser tagName=''}}
|
||||||
|
<div class='post-contents'>
|
||||||
|
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||||
|
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='reviewable-actions'>
|
||||||
|
{{d-button
|
||||||
|
class="btn-danger"
|
||||||
|
label="review.delete"
|
||||||
|
icon="trash-alt"
|
||||||
|
action=(action "deletePending" pending) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if model.queued_posts_count}}
|
||||||
<div class="has-pending-posts">
|
<div class="has-pending-posts">
|
||||||
<div>
|
<div>
|
||||||
{{{i18n "review.topic_has_pending" count=model.pending_posts_count}}}
|
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||||
|
@ -266,3 +266,17 @@ a.topic-featured-link {
|
|||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-area {
|
||||||
|
.pending-posts {
|
||||||
|
max-width: calc(
|
||||||
|
#{$topic-body-width} + #{$topic-avatar-width} + #{$topic-body-width-padding *
|
||||||
|
2}
|
||||||
|
);
|
||||||
|
.reviewable-item {
|
||||||
|
.post-body {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -96,6 +96,15 @@ class ReviewablesController < ApplicationController
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
reviewable = Reviewable.find_by(id: params[:reviewable_id], created_by: current_user)
|
||||||
|
raise Discourse::NotFound.new if reviewable.blank?
|
||||||
|
|
||||||
|
reviewable.perform(current_user, :delete)
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
reviewable = find_reviewable
|
reviewable = find_reviewable
|
||||||
editable = reviewable.editable_for(guardian)
|
editable = reviewable.editable_for(guardian)
|
||||||
|
@ -482,8 +482,9 @@ end
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
# index_reviewables_on_status_and_score (status,score)
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
# index_reviewables_on_status_and_type (status,type)
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
#
|
#
|
||||||
|
@ -304,8 +304,9 @@ end
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
# index_reviewables_on_status_and_score (status,score)
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
# index_reviewables_on_status_and_type (status,type)
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
#
|
#
|
||||||
|
@ -9,18 +9,20 @@ class ReviewableQueuedPost < Reviewable
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_actions(actions, guardian, args)
|
def build_actions(actions, guardian, args)
|
||||||
return unless guardian.is_staff?
|
if guardian.is_staff?
|
||||||
|
actions.add(:approve) unless approved?
|
||||||
|
actions.add(:reject) unless rejected?
|
||||||
|
|
||||||
actions.add(:approve) unless approved?
|
if pending? && guardian.can_delete_user?(created_by)
|
||||||
actions.add(:reject) unless rejected?
|
actions.add(:delete_user) do |action|
|
||||||
|
action.icon = 'trash-alt'
|
||||||
if pending? && guardian.can_delete_user?(created_by)
|
action.label = 'reviewables.actions.delete_user.title'
|
||||||
actions.add(:delete_user) do |action|
|
action.confirm_message = 'reviewables.actions.delete_user.confirm'
|
||||||
action.icon = 'trash-alt'
|
end
|
||||||
action.label = 'reviewables.actions.delete_user.title'
|
|
||||||
action.confirm_message = 'reviewables.actions.delete_user.confirm'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
actions.add(:delete) if guardian.can_delete?(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_editable_fields(fields, guardian, args)
|
def build_editable_fields(fields, guardian, args)
|
||||||
@ -82,6 +84,11 @@ class ReviewableQueuedPost < Reviewable
|
|||||||
create_result(:success, :rejected)
|
create_result(:success, :rejected)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform_delete(performed_by, args)
|
||||||
|
create_result(:success, :deleted)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def perform_delete_user(performed_by, args)
|
def perform_delete_user(performed_by, args)
|
||||||
delete_options = {
|
delete_options = {
|
||||||
context: I18n.t('reviewables.actions.delete_user.reason'),
|
context: I18n.t('reviewables.actions.delete_user.reason'),
|
||||||
@ -126,8 +133,9 @@ end
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
# index_reviewables_on_status_and_score (status,score)
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
# index_reviewables_on_status_and_type (status,type)
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
#
|
#
|
||||||
|
@ -86,6 +86,7 @@ end
|
|||||||
# meta_topic_id :integer
|
# meta_topic_id :integer
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# reason :string
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
@ -87,8 +87,9 @@ end
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
# index_reviewables_on_status_and_score (status,score)
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
# index_reviewables_on_status_and_type (status,type)
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
#
|
#
|
||||||
|
@ -38,30 +38,29 @@ class TopicLink < ActiveRecord::Base
|
|||||||
def self.topic_map(guardian, topic_id)
|
def self.topic_map(guardian, topic_id)
|
||||||
|
|
||||||
# Sam: complicated reports are really hard in AR
|
# Sam: complicated reports are really hard in AR
|
||||||
builder = DB.build <<-SQL
|
builder = DB.build(<<~SQL)
|
||||||
SELECT ftl.url,
|
SELECT ftl.url,
|
||||||
COALESCE(ft.title, ftl.title) AS title,
|
COALESCE(ft.title, ftl.title) AS title,
|
||||||
ftl.link_topic_id,
|
ftl.link_topic_id,
|
||||||
ftl.reflection,
|
ftl.reflection,
|
||||||
ftl.internal,
|
ftl.internal,
|
||||||
ftl.domain,
|
ftl.domain,
|
||||||
MIN(ftl.user_id) AS user_id,
|
MIN(ftl.user_id) AS user_id,
|
||||||
SUM(clicks) AS clicks
|
SUM(clicks) AS clicks
|
||||||
FROM topic_links AS ftl
|
FROM topic_links AS ftl
|
||||||
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
|
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
|
||||||
LEFT JOIN categories AS c ON c.id = ft.category_id
|
LEFT JOIN categories AS c ON c.id = ft.category_id
|
||||||
/*where*/
|
/*where*/
|
||||||
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
|
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
|
||||||
ORDER BY clicks DESC, count(*) DESC
|
ORDER BY clicks DESC, count(*) DESC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
builder.where('ftl.topic_id = :topic_id', topic_id: topic_id)
|
builder.where('ftl.topic_id = :topic_id', topic_id: topic_id)
|
||||||
builder.where('ft.deleted_at IS NULL')
|
builder.where('ft.deleted_at IS NULL')
|
||||||
# note that ILIKE means "case insensitive LIKE"
|
# note that ILIKE means "case insensitive LIKE"
|
||||||
builder.where("NOT(ftl.url ILIKE '%.png' OR ftl.url ILIKE '%.jpg' OR ftl.url ILIKE '%.gif')")
|
builder.where("NOT(ftl.url ILIKE '%.png' OR ftl.url ILIKE '%.jpg' OR ftl.url ILIKE '%.gif')")
|
||||||
builder.where("COALESCE(ft.archetype, 'regular') <> :archetype", archetype: Archetype.private_message)
|
builder.where("COALESCE(ft.archetype, 'regular') <> :archetype", archetype: Archetype.private_message)
|
||||||
# do not show links with 0 click
|
|
||||||
builder.where("clicks > 0")
|
builder.where("clicks > 0")
|
||||||
|
|
||||||
builder.secure_category(guardian.secure_category_ids)
|
builder.secure_category(guardian.secure_category_ids)
|
||||||
|
@ -8,6 +8,8 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||||||
:pending_count,
|
:pending_count,
|
||||||
:reason
|
:reason
|
||||||
|
|
||||||
|
has_one :pending_post, serializer: TopicPendingPostSerializer, root: false, embed: :objects
|
||||||
|
|
||||||
def post
|
def post
|
||||||
post_serializer = PostSerializer.new(object.post, scope: scope, root: false)
|
post_serializer = PostSerializer.new(object.post, scope: scope, root: false)
|
||||||
post_serializer.draft_sequence = DraftSequence.current(scope.user, object.post.topic.draft_key)
|
post_serializer.draft_sequence = DraftSequence.current(scope.user, object.post.topic.draft_key)
|
||||||
@ -39,7 +41,7 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def include_reason?
|
def include_reason?
|
||||||
reason.present?
|
scope.is_staff? && reason.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def action
|
def action
|
||||||
@ -50,6 +52,14 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||||||
object.pending_count
|
object.pending_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pending_post
|
||||||
|
object.reviewable
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_pending_post?
|
||||||
|
object.reviewable.present?
|
||||||
|
end
|
||||||
|
|
||||||
def include_pending_count?
|
def include_pending_count?
|
||||||
pending_count.present?
|
pending_count.present?
|
||||||
end
|
end
|
||||||
|
12
app/serializers/topic_pending_post_serializer.rb
Normal file
12
app/serializers/topic_pending_post_serializer.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class TopicPendingPostSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :raw, :created_at
|
||||||
|
|
||||||
|
def raw
|
||||||
|
object.payload['raw']
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_raw?
|
||||||
|
object.payload && object.payload['raw'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
140
app/serializers/topic_view_details_serializer.rb
Normal file
140
app/serializers/topic_view_details_serializer.rb
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
class TopicViewDetailsSerializer < ApplicationSerializer
|
||||||
|
|
||||||
|
def self.can_attributes
|
||||||
|
[:can_move_posts,
|
||||||
|
:can_edit,
|
||||||
|
:can_delete,
|
||||||
|
:can_recover,
|
||||||
|
:can_remove_allowed_users,
|
||||||
|
:can_invite_to,
|
||||||
|
:can_invite_via_email,
|
||||||
|
:can_create_post,
|
||||||
|
:can_reply_as_new_topic,
|
||||||
|
:can_flag_topic,
|
||||||
|
:can_convert_topic]
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes(
|
||||||
|
:notification_level,
|
||||||
|
:notifications_reason_id,
|
||||||
|
*can_attributes,
|
||||||
|
:can_remove_self_id,
|
||||||
|
:participants,
|
||||||
|
:allowed_users
|
||||||
|
)
|
||||||
|
|
||||||
|
has_one :created_by, serializer: BasicUserSerializer, embed: :objects
|
||||||
|
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
|
||||||
|
has_many :links, serializer: TopicLinkSerializer, embed: :objects
|
||||||
|
has_many :participants, serializer: TopicPostCountSerializer, embed: :objects
|
||||||
|
has_many :allowed_users, serializer: BasicUserSerializer, embed: :objects
|
||||||
|
has_many :allowed_groups, serializer: BasicGroupSerializer, embed: :objects
|
||||||
|
|
||||||
|
def participants
|
||||||
|
object.post_counts_by_user.reject { |p| object.participants[p].blank? }.map do |pc|
|
||||||
|
{ user: object.participants[pc[0]], post_count: pc[1] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_participants?
|
||||||
|
object.post_counts_by_user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_links?
|
||||||
|
object.links.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_by
|
||||||
|
object.topic.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_poster
|
||||||
|
object.topic.last_poster
|
||||||
|
end
|
||||||
|
|
||||||
|
def notification_level
|
||||||
|
object.topic_user&.notification_level || TopicUser.notification_levels[:regular]
|
||||||
|
end
|
||||||
|
|
||||||
|
def notifications_reason_id
|
||||||
|
object.topic_user.notifications_reason_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_notifications_reason_id?
|
||||||
|
object.topic_user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# confusingly this is an id, not a bool like all other `can` methods
|
||||||
|
def can_remove_self_id
|
||||||
|
scope.user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_remove_self_id?
|
||||||
|
scope.can_remove_allowed_users?(object.topic, scope.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
can_attributes.each do |ca|
|
||||||
|
define_method(ca) { true }
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_move_posts?
|
||||||
|
scope.can_move_posts?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_edit?
|
||||||
|
scope.can_edit?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_delete?
|
||||||
|
scope.can_delete?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_recover?
|
||||||
|
scope.can_recover_topic?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_remove_allowed_users?
|
||||||
|
scope.can_remove_allowed_users?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_invite_to?
|
||||||
|
scope.can_invite_to?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_invite_via_email?
|
||||||
|
scope.can_invite_via_email?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_create_post?
|
||||||
|
scope.can_create?(Post, object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_reply_as_new_topic?
|
||||||
|
scope.can_reply_as_new_topic?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_flag_topic?
|
||||||
|
object.actions_summary.any? { |a| a[:can_act] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_can_convert_topic?
|
||||||
|
scope.can_convert_topic?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_users
|
||||||
|
object.topic.allowed_users.reject { |user| object.group_allowed_user_ids.include?(user.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_allowed_users?
|
||||||
|
object.personal_message
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_groups
|
||||||
|
object.topic.allowed_groups
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_allowed_groups?
|
||||||
|
object.personal_message
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -50,7 +50,6 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
:posted,
|
:posted,
|
||||||
:unpinned,
|
:unpinned,
|
||||||
:pinned,
|
:pinned,
|
||||||
:details,
|
|
||||||
:current_post_number,
|
:current_post_number,
|
||||||
:highest_post_number,
|
:highest_post_number,
|
||||||
:last_read_post_number,
|
:last_read_post_number,
|
||||||
@ -70,66 +69,14 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
:participant_count,
|
:participant_count,
|
||||||
:destination_category_id,
|
:destination_category_id,
|
||||||
:pm_with_non_human_user,
|
:pm_with_non_human_user,
|
||||||
:pending_posts_count
|
:queued_posts_count
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Split off into proper object / serializer
|
has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects
|
||||||
|
has_many :pending_posts, serializer: TopicPendingPostSerializer, root: false, embed: :objects
|
||||||
|
|
||||||
def details
|
def details
|
||||||
topic = object.topic
|
object
|
||||||
|
|
||||||
result = {
|
|
||||||
created_by: BasicUserSerializer.new(topic.user, scope: scope, root: false),
|
|
||||||
last_poster: BasicUserSerializer.new(topic.last_poster, scope: scope, root: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if private_message?(topic)
|
|
||||||
allowed_user_ids = Set.new
|
|
||||||
|
|
||||||
result[:allowed_groups] = object.topic.allowed_groups.map do |group|
|
|
||||||
allowed_user_ids.merge(GroupUser.where(group: group).pluck(:user_id))
|
|
||||||
BasicGroupSerializer.new(group, scope: scope, root: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
result[:allowed_users] = object.topic.allowed_users.select do |user|
|
|
||||||
!allowed_user_ids.include?(user.id)
|
|
||||||
end.map! do |user|
|
|
||||||
BasicUserSerializer.new(user, scope: scope, root: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if object.post_counts_by_user.present?
|
|
||||||
participants = object.post_counts_by_user.reject { |p| object.participants[p].blank? }.map do |pc|
|
|
||||||
TopicPostCountSerializer.new({ user: object.participants[pc[0]], post_count: pc[1] }, scope: scope, root: false)
|
|
||||||
end
|
|
||||||
result[:participants] = participants if participants.length > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if object.links.present?
|
|
||||||
result[:links] = object.links.map do |user|
|
|
||||||
TopicLinkSerializer.new(user, scope: scope, root: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if has_topic_user?
|
|
||||||
result[:notification_level] = object.topic_user.notification_level
|
|
||||||
result[:notifications_reason_id] = object.topic_user.notifications_reason_id
|
|
||||||
else
|
|
||||||
result[:notification_level] = TopicUser.notification_levels[:regular]
|
|
||||||
end
|
|
||||||
|
|
||||||
result[:can_move_posts] = true if scope.can_move_posts?(object.topic)
|
|
||||||
result[:can_edit] = true if scope.can_edit?(object.topic)
|
|
||||||
result[:can_delete] = true if scope.can_delete?(object.topic)
|
|
||||||
result[:can_recover] = true if scope.can_recover_topic?(object.topic)
|
|
||||||
result[:can_remove_allowed_users] = true if scope.can_remove_allowed_users?(object.topic)
|
|
||||||
result[:can_remove_self_id] = scope.user.id if scope.can_remove_allowed_users?(object.topic, scope.user)
|
|
||||||
result[:can_invite_to] = true if scope.can_invite_to?(object.topic)
|
|
||||||
result[:can_invite_via_email] = true if scope.can_invite_via_email?(object.topic)
|
|
||||||
result[:can_create_post] = true if scope.can_create?(Post, object.topic)
|
|
||||||
result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic)
|
|
||||||
result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] }
|
|
||||||
result[:can_convert_topic] = true if scope.can_convert_topic?(object.topic)
|
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_bus_last_id
|
def message_bus_last_id
|
||||||
@ -141,7 +88,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def is_warning
|
def is_warning
|
||||||
private_message?(object.topic) && object.topic.subtype == TopicSubtype.moderator_warning
|
object.personal_message && object.topic.subtype == TopicSubtype.moderator_warning
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_is_warning?
|
def include_is_warning?
|
||||||
@ -161,7 +108,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def include_message_archived?
|
def include_message_archived?
|
||||||
private_message?(object.topic)
|
object.personal_message
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_archived
|
def message_archived
|
||||||
@ -214,16 +161,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def actions_summary
|
def actions_summary
|
||||||
result = []
|
object.actions_summary
|
||||||
return [] unless post = object.posts&.first
|
|
||||||
PostActionType.topic_flag_types.each do |sym, id|
|
|
||||||
result << { id: id,
|
|
||||||
count: 0,
|
|
||||||
hidden: false,
|
|
||||||
can_act: scope.post_can_act?(post, sym) }
|
|
||||||
# TODO: other keys? :can_defer_flags, :acted, :can_undo
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_deleted
|
def has_deleted
|
||||||
@ -276,7 +214,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def include_pm_with_non_human_user?
|
def include_pm_with_non_human_user?
|
||||||
private_message?(object.topic)
|
object.personal_message
|
||||||
end
|
end
|
||||||
|
|
||||||
def pm_with_non_human_user
|
def pm_with_non_human_user
|
||||||
@ -297,18 +235,15 @@ class TopicViewSerializer < ApplicationSerializer
|
|||||||
object.topic.shared_draft.present?
|
object.topic.shared_draft.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pending_posts_count
|
def include_pending_posts?
|
||||||
ReviewableQueuedPost.viewable_by(scope.user).where(topic_id: object.topic.id).pending.count
|
scope.authenticated? && object.queued_posts_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_pending_posts_count?
|
def queued_posts_count
|
||||||
scope.is_staff? && NewPostManager.queue_enabled?
|
object.queued_posts_count
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def include_queued_posts_count?
|
||||||
|
scope.is_staff? && object.queued_posts_enabled
|
||||||
def private_message?(topic)
|
|
||||||
@private_message ||= topic.private_message?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -359,6 +359,8 @@ en:
|
|||||||
placeholder: "type the message title here"
|
placeholder: "type the message title here"
|
||||||
|
|
||||||
review:
|
review:
|
||||||
|
awaiting_approval: "Awaiting Approval"
|
||||||
|
delete: "Delete"
|
||||||
settings:
|
settings:
|
||||||
saved: "Saved"
|
saved: "Saved"
|
||||||
save_changes: "Save Changes"
|
save_changes: "Save Changes"
|
||||||
|
@ -325,6 +325,7 @@ Discourse::Application.routes.draw do
|
|||||||
action_id: /[a-z\_]+/
|
action_id: /[a-z\_]+/
|
||||||
}
|
}
|
||||||
put "review/:reviewable_id" => "reviewables#update", constraints: { reviewable_id: /\d+/ }
|
put "review/:reviewable_id" => "reviewables#update", constraints: { reviewable_id: /\d+/ }
|
||||||
|
delete "review/:reviewable_id" => "reviewables#destroy", constraints: { reviewable_id: /\d+/ }
|
||||||
|
|
||||||
get "session/sso" => "session#sso"
|
get "session/sso" => "session#sso"
|
||||||
get "session/sso_login" => "session#sso_login"
|
get "session/sso_login" => "session#sso_login"
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddCreatedByIndexToReviewables < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_index :reviewables, [:topic_id, :status, :created_by_id]
|
||||||
|
end
|
||||||
|
end
|
@ -174,6 +174,12 @@ class Guardian
|
|||||||
SiteSetting.enable_badges && is_staff?
|
SiteSetting.enable_badges && is_staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_delete_reviewable_queued_post?(reviewable)
|
||||||
|
reviewable.present? &&
|
||||||
|
authenticated? &&
|
||||||
|
reviewable.created_by_id == @user.id
|
||||||
|
end
|
||||||
|
|
||||||
def can_see_group?(group)
|
def can_see_group?(group)
|
||||||
return false if group.blank?
|
return false if group.blank?
|
||||||
return true if group.visibility_level == Group.visibility_levels[:public]
|
return true if group.visibility_level == Group.visibility_levels[:public]
|
||||||
|
@ -6,8 +6,26 @@ require_dependency 'gaps'
|
|||||||
class TopicView
|
class TopicView
|
||||||
MEGA_TOPIC_POSTS_COUNT = 10000
|
MEGA_TOPIC_POSTS_COUNT = 10000
|
||||||
|
|
||||||
attr_reader :topic, :posts, :guardian, :filtered_posts, :chunk_size, :print, :message_bus_last_id
|
attr_reader(
|
||||||
attr_accessor :draft, :draft_key, :draft_sequence, :user_custom_fields, :post_custom_fields, :post_number
|
:topic,
|
||||||
|
:posts,
|
||||||
|
:guardian,
|
||||||
|
:filtered_posts,
|
||||||
|
:chunk_size,
|
||||||
|
:print,
|
||||||
|
:message_bus_last_id,
|
||||||
|
:queued_posts_enabled,
|
||||||
|
:personal_message
|
||||||
|
)
|
||||||
|
|
||||||
|
attr_accessor(
|
||||||
|
:draft,
|
||||||
|
:draft_key,
|
||||||
|
:draft_sequence,
|
||||||
|
:user_custom_fields,
|
||||||
|
:post_custom_fields,
|
||||||
|
:post_number
|
||||||
|
)
|
||||||
|
|
||||||
def self.print_chunk_size
|
def self.print_chunk_size
|
||||||
1000
|
1000
|
||||||
@ -81,6 +99,9 @@ class TopicView
|
|||||||
|
|
||||||
@draft_key = @topic.draft_key
|
@draft_key = @topic.draft_key
|
||||||
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
||||||
|
|
||||||
|
@queued_posts_enabled = NewPostManager.queue_enabled?
|
||||||
|
@personal_message = @topic.private_message?
|
||||||
end
|
end
|
||||||
|
|
||||||
def canonical_path
|
def canonical_path
|
||||||
@ -378,6 +399,13 @@ class TopicView
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_allowed_user_ids
|
||||||
|
return @group_allowed_user_ids unless @group_allowed_user_ids.nil?
|
||||||
|
|
||||||
|
group_ids = @topic.allowed_groups.map(&:id)
|
||||||
|
@group_allowed_user_ids = Set.new(GroupUser.where(group_id: group_ids).pluck('distinct user_id'))
|
||||||
|
end
|
||||||
|
|
||||||
def all_post_actions
|
def all_post_actions
|
||||||
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
||||||
end
|
end
|
||||||
@ -390,6 +418,27 @@ class TopicView
|
|||||||
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pending_posts
|
||||||
|
ReviewableQueuedPost.pending.where(created_by: @user, topic: @topic).order(:created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def actions_summary
|
||||||
|
return @actions_summary unless @actions_summary.nil?
|
||||||
|
|
||||||
|
@actions_summary = []
|
||||||
|
return @actions_summary unless post = posts&.first
|
||||||
|
PostActionType.topic_flag_types.each do |sym, id|
|
||||||
|
@actions_summary << {
|
||||||
|
id: id,
|
||||||
|
count: 0,
|
||||||
|
hidden: false,
|
||||||
|
can_act: @guardian.post_can_act?(post, sym)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@actions_summary
|
||||||
|
end
|
||||||
|
|
||||||
def link_counts
|
def link_counts
|
||||||
@link_counts ||= TopicLink.counts_for(@guardian, @topic, posts)
|
@link_counts ||= TopicLink.counts_for(@guardian, @topic, posts)
|
||||||
end
|
end
|
||||||
@ -493,6 +542,10 @@ class TopicView
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def queued_posts_count
|
||||||
|
ReviewableQueuedPost.viewable_by(@user).where(topic_id: @topic.id).pending.count
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def read_posts_set
|
def read_posts_set
|
||||||
|
@ -803,6 +803,10 @@ describe PostsController do
|
|||||||
rp = ReviewableQueuedPost.find_by(created_by: user)
|
rp = ReviewableQueuedPost.find_by(created_by: user)
|
||||||
expect(rp.reviewable_scores.first.reason).to eq('fast_typer')
|
expect(rp.reviewable_scores.first.reason).to eq('fast_typer')
|
||||||
|
|
||||||
|
expect(parsed['pending_post']).to be_present
|
||||||
|
expect(parsed['pending_post']['id']).to eq(rp.id)
|
||||||
|
expect(parsed['pending_post']['raw']).to eq("this is the test content")
|
||||||
|
|
||||||
mod = Fabricate(:moderator)
|
mod = Fabricate(:moderator)
|
||||||
rp.perform(mod, :approve)
|
rp.perform(mod, :approve)
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@ describe ReviewablesController do
|
|||||||
get "/review/settings.json"
|
get "/review/settings.json"
|
||||||
expect(response.code).to eq("403")
|
expect(response.code).to eq("403")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "denies deleting" do
|
||||||
|
delete "/review/123"
|
||||||
|
expect(response.code).to eq("403")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "regular user" do
|
context "regular user" do
|
||||||
@ -452,6 +457,32 @@ describe ReviewablesController do
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "#destroy" do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 404 if the reviewable doesn't exist" do
|
||||||
|
delete "/review/1234.json"
|
||||||
|
expect(response.code).to eq("404")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 404 if the user can't see the reviewable" do
|
||||||
|
queued_post = Fabricate(:reviewable_queued_post)
|
||||||
|
delete "/review/#{queued_post.id}.json"
|
||||||
|
expect(response.code).to eq("404")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 200 if the user can delete the reviewable" do
|
||||||
|
queued_post = Fabricate(:reviewable_queued_post, created_by: user)
|
||||||
|
delete "/review/#{queued_post.id}.json"
|
||||||
|
expect(response.code).to eq("200")
|
||||||
|
expect(queued_post.reload).to be_deleted
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,8 @@ require 'rails_helper'
|
|||||||
describe TopicViewSerializer do
|
describe TopicViewSerializer do
|
||||||
def serialize_topic(topic, user_arg)
|
def serialize_topic(topic, user_arg)
|
||||||
topic_view = TopicView.new(topic.id, user_arg)
|
topic_view = TopicView.new(topic.id, user_arg)
|
||||||
TopicViewSerializer.new(topic_view, scope: Guardian.new(user_arg), root: false).as_json
|
serializer = TopicViewSerializer.new(topic_view, scope: Guardian.new(user_arg), root: false).as_json
|
||||||
|
JSON.parse(MultiJson.dump(serializer)).deep_symbolize_keys!
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@ -53,7 +54,7 @@ describe TopicViewSerializer do
|
|||||||
it 'should include suggested topics' do
|
it 'should include suggested topics' do
|
||||||
json = serialize_topic(topic, user)
|
json = serialize_topic(topic, user)
|
||||||
|
|
||||||
expect(json[:suggested_topics].first.id).to eq(topic2.id)
|
expect(json[:suggested_topics].first[:id]).to eq(topic2.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -130,4 +131,84 @@ describe TopicViewSerializer do
|
|||||||
expect(json[:tags]).to eq([])
|
expect(json[:tags]).to eq([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "pending posts" do
|
||||||
|
context "when the queue is enabled" do
|
||||||
|
before do
|
||||||
|
SiteSetting.approve_post_count = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:queued_post) do
|
||||||
|
ReviewableQueuedPost.needs_review!(
|
||||||
|
topic: topic,
|
||||||
|
payload: { raw: "hello my raw contents" },
|
||||||
|
created_by: user
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a pending_posts_count when the queue is enabled" do
|
||||||
|
json = serialize_topic(topic, admin)
|
||||||
|
expect(json[:queued_posts_count]).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a user's pending posts" do
|
||||||
|
json = serialize_topic(topic, user)
|
||||||
|
expect(json[:queued_posts_count]).to be_nil
|
||||||
|
|
||||||
|
post = json[:pending_posts].find { |p| p[:id] = queued_post.id }
|
||||||
|
expect(post[:raw]).to eq("hello my raw contents")
|
||||||
|
expect(post).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without an enabled queue" do
|
||||||
|
it "returns nil for the count" do
|
||||||
|
json = serialize_topic(topic, admin)
|
||||||
|
expect(json[:queued_posts_count]).to be_nil
|
||||||
|
expect(json[:pending_posts]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "details" do
|
||||||
|
it "returns the details object" do
|
||||||
|
PostCreator.create!(user, topic_id: topic.id, raw: "this is my post content")
|
||||||
|
topic.topic_links.create!(user: user, url: 'https://discourse.org', domain: 'discourse.org', clicks: 100)
|
||||||
|
json = serialize_topic(topic, admin)
|
||||||
|
|
||||||
|
details = json[:details]
|
||||||
|
expect(details).to be_present
|
||||||
|
expect(details[:created_by][:id]).to eq(topic.user_id)
|
||||||
|
expect(details[:last_poster][:id]).to eq(user.id)
|
||||||
|
expect(details[:notification_level]).to be_present
|
||||||
|
expect(details[:can_move_posts]).to eq(true)
|
||||||
|
expect(details[:can_flag_topic]).to eq(true)
|
||||||
|
expect(details[:links][0][:clicks]).to eq(100)
|
||||||
|
|
||||||
|
participant = details[:participants].find { |p| p[:id] == user.id }
|
||||||
|
expect(participant[:post_count]).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns extra fields for a personal message" do
|
||||||
|
group = Fabricate(:group)
|
||||||
|
GroupUser.create(group: group, user: user)
|
||||||
|
GroupUser.create(group: group, user: admin)
|
||||||
|
|
||||||
|
group2 = Fabricate(:group)
|
||||||
|
GroupUser.create(group: group2, user: user)
|
||||||
|
|
||||||
|
pm = Fabricate(:private_message_topic)
|
||||||
|
pm.update(archetype: 'private_message')
|
||||||
|
pm.topic_allowed_groups.create!(group: group)
|
||||||
|
pm.topic_allowed_groups.create!(group: group2)
|
||||||
|
|
||||||
|
json = serialize_topic(pm, admin)
|
||||||
|
|
||||||
|
details = json[:details]
|
||||||
|
expect(details[:can_remove_self_id]).to eq(admin.id)
|
||||||
|
expect(details[:allowed_users].find { |au| au[:id] == pm.user_id }).to be_present
|
||||||
|
expect(details[:allowed_groups].find { |ag| ag[:id] == group.id }).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -255,6 +255,8 @@ QUnit.test("Posting on a different topic", async assert => {
|
|||||||
QUnit.test("Create an enqueued Reply", async assert => {
|
QUnit.test("Create an enqueued Reply", async assert => {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
assert.notOk(find(".pending-posts .reviewable-item").length);
|
||||||
|
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
assert.ok(exists(".d-editor-input"), "the composer input is visible");
|
assert.ok(exists(".d-editor-input"), "the composer input is visible");
|
||||||
assert.ok(!exists("#reply-title"), "there is no title since this is a reply");
|
assert.ok(!exists("#reply-title"), "there is no title since this is a reply");
|
||||||
@ -270,6 +272,8 @@ QUnit.test("Create an enqueued Reply", async assert => {
|
|||||||
|
|
||||||
await click(".modal-footer button");
|
await click(".modal-footer button");
|
||||||
assert.ok(invisible(".d-modal"), "the modal can be dismissed");
|
assert.ok(invisible(".d-modal"), "the modal can be dismissed");
|
||||||
|
|
||||||
|
assert.ok(find(".pending-posts .reviewable-item").length);
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("Edit the first post", async assert => {
|
QUnit.test("Edit the first post", async assert => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/*jshint maxlen:10000000 */
|
/*jshint maxlen:10000000 */
|
||||||
export default {
|
export default {
|
||||||
"/t/280/1.json": {
|
"/t/280/1.json": {
|
||||||
|
pending_posts: [],
|
||||||
post_stream: {
|
post_stream: {
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
|
@ -430,7 +430,14 @@ export default function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.raw === "enqueue this content please") {
|
if (data.raw === "enqueue this content please") {
|
||||||
return response(200, { success: true, action: "enqueued" });
|
return response(200, {
|
||||||
|
success: true,
|
||||||
|
action: "enqueued",
|
||||||
|
pending_post: {
|
||||||
|
id: 1234,
|
||||||
|
raw: data.raw
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return response(200, {
|
return response(200, {
|
||||||
|
Reference in New Issue
Block a user