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:
parent
1760069419
commit
a5d9afe397
|
@ -679,6 +679,13 @@ export default Ember.Controller.extend({
|
|||
.then(result => {
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
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.close();
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
|
|
|
@ -202,6 +202,14 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
|||
},
|
||||
|
||||
actions: {
|
||||
deletePending(pending) {
|
||||
return ajax(`/review/${pending.id}`, { type: "DELETE" })
|
||||
.then(() => {
|
||||
this.get("model.pending_posts").removeObject(pending);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
showPostFlags(post) {
|
||||
return this.send("showFlags", post);
|
||||
},
|
||||
|
|
|
@ -211,10 +211,41 @@
|
|||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#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>
|
||||
{{{i18n "review.topic_has_pending" count=model.pending_posts_count}}}
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
</div>
|
||||
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
|
|
|
@ -266,3 +266,17 @@ a.topic-featured-link {
|
|||
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
|
||||
|
||||
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
|
||||
reviewable = find_reviewable
|
||||
editable = reviewable.editable_for(guardian)
|
||||
|
|
|
@ -482,8 +482,9 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# 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
|
||||
#
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# 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
|
||||
|
||||
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?
|
||||
actions.add(:reject) unless rejected?
|
||||
|
||||
if pending? && guardian.can_delete_user?(created_by)
|
||||
actions.add(:delete_user) do |action|
|
||||
action.icon = 'trash-alt'
|
||||
action.label = 'reviewables.actions.delete_user.title'
|
||||
action.confirm_message = 'reviewables.actions.delete_user.confirm'
|
||||
if pending? && guardian.can_delete_user?(created_by)
|
||||
actions.add(:delete_user) do |action|
|
||||
action.icon = 'trash-alt'
|
||||
action.label = 'reviewables.actions.delete_user.title'
|
||||
action.confirm_message = 'reviewables.actions.delete_user.confirm'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
actions.add(:delete) if guardian.can_delete?(self)
|
||||
end
|
||||
|
||||
def build_editable_fields(fields, guardian, args)
|
||||
|
@ -82,6 +84,11 @@ class ReviewableQueuedPost < Reviewable
|
|||
create_result(:success, :rejected)
|
||||
end
|
||||
|
||||
def perform_delete(performed_by, args)
|
||||
create_result(:success, :deleted)
|
||||
end
|
||||
|
||||
|
||||
def perform_delete_user(performed_by, args)
|
||||
delete_options = {
|
||||
context: I18n.t('reviewables.actions.delete_user.reason'),
|
||||
|
@ -126,8 +133,9 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# 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
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# reason :string
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -87,8 +87,9 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||
# index_reviewables_on_status_and_score (status,score)
|
||||
# index_reviewables_on_status_and_type (status,type)
|
||||
# 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)
|
||||
|
||||
# Sam: complicated reports are really hard in AR
|
||||
builder = DB.build <<-SQL
|
||||
SELECT ftl.url,
|
||||
COALESCE(ft.title, ftl.title) AS title,
|
||||
ftl.link_topic_id,
|
||||
ftl.reflection,
|
||||
ftl.internal,
|
||||
ftl.domain,
|
||||
MIN(ftl.user_id) AS user_id,
|
||||
SUM(clicks) AS clicks
|
||||
FROM topic_links AS ftl
|
||||
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
|
||||
LEFT JOIN categories AS c ON c.id = ft.category_id
|
||||
/*where*/
|
||||
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
|
||||
ORDER BY clicks DESC, count(*) DESC
|
||||
LIMIT 50
|
||||
SQL
|
||||
builder = DB.build(<<~SQL)
|
||||
SELECT ftl.url,
|
||||
COALESCE(ft.title, ftl.title) AS title,
|
||||
ftl.link_topic_id,
|
||||
ftl.reflection,
|
||||
ftl.internal,
|
||||
ftl.domain,
|
||||
MIN(ftl.user_id) AS user_id,
|
||||
SUM(clicks) AS clicks
|
||||
FROM topic_links AS ftl
|
||||
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
|
||||
LEFT JOIN categories AS c ON c.id = ft.category_id
|
||||
/*where*/
|
||||
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
|
||||
ORDER BY clicks DESC, count(*) DESC
|
||||
LIMIT 50
|
||||
SQL
|
||||
|
||||
builder.where('ftl.topic_id = :topic_id', topic_id: topic_id)
|
||||
builder.where('ft.deleted_at IS NULL')
|
||||
# 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("COALESCE(ft.archetype, 'regular') <> :archetype", archetype: Archetype.private_message)
|
||||
# do not show links with 0 click
|
||||
builder.where("clicks > 0")
|
||||
|
||||
builder.secure_category(guardian.secure_category_ids)
|
||||
|
|
|
@ -8,6 +8,8 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||
:pending_count,
|
||||
:reason
|
||||
|
||||
has_one :pending_post, serializer: TopicPendingPostSerializer, root: false, embed: :objects
|
||||
|
||||
def post
|
||||
post_serializer = PostSerializer.new(object.post, scope: scope, root: false)
|
||||
post_serializer.draft_sequence = DraftSequence.current(scope.user, object.post.topic.draft_key)
|
||||
|
@ -39,7 +41,7 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def include_reason?
|
||||
reason.present?
|
||||
scope.is_staff? && reason.present?
|
||||
end
|
||||
|
||||
def action
|
||||
|
@ -50,6 +52,14 @@ class NewPostResultSerializer < ApplicationSerializer
|
|||
object.pending_count
|
||||
end
|
||||
|
||||
def pending_post
|
||||
object.reviewable
|
||||
end
|
||||
|
||||
def include_pending_post?
|
||||
object.reviewable.present?
|
||||
end
|
||||
|
||||
def include_pending_count?
|
||||
pending_count.present?
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
:unpinned,
|
||||
:pinned,
|
||||
:details,
|
||||
:current_post_number,
|
||||
:highest_post_number,
|
||||
:last_read_post_number,
|
||||
|
@ -70,66 +69,14 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
:participant_count,
|
||||
:destination_category_id,
|
||||
: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
|
||||
topic = object.topic
|
||||
|
||||
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
|
||||
object
|
||||
end
|
||||
|
||||
def message_bus_last_id
|
||||
|
@ -141,7 +88,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def is_warning
|
||||
private_message?(object.topic) && object.topic.subtype == TopicSubtype.moderator_warning
|
||||
object.personal_message && object.topic.subtype == TopicSubtype.moderator_warning
|
||||
end
|
||||
|
||||
def include_is_warning?
|
||||
|
@ -161,7 +108,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def include_message_archived?
|
||||
private_message?(object.topic)
|
||||
object.personal_message
|
||||
end
|
||||
|
||||
def message_archived
|
||||
|
@ -214,16 +161,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def actions_summary
|
||||
result = []
|
||||
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
|
||||
object.actions_summary
|
||||
end
|
||||
|
||||
def has_deleted
|
||||
|
@ -276,7 +214,7 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def include_pm_with_non_human_user?
|
||||
private_message?(object.topic)
|
||||
object.personal_message
|
||||
end
|
||||
|
||||
def pm_with_non_human_user
|
||||
|
@ -297,18 +235,15 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
object.topic.shared_draft.present?
|
||||
end
|
||||
|
||||
def pending_posts_count
|
||||
ReviewableQueuedPost.viewable_by(scope.user).where(topic_id: object.topic.id).pending.count
|
||||
def include_pending_posts?
|
||||
scope.authenticated? && object.queued_posts_enabled
|
||||
end
|
||||
|
||||
def include_pending_posts_count?
|
||||
scope.is_staff? && NewPostManager.queue_enabled?
|
||||
def queued_posts_count
|
||||
object.queued_posts_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def private_message?(topic)
|
||||
@private_message ||= topic.private_message?
|
||||
def include_queued_posts_count?
|
||||
scope.is_staff? && object.queued_posts_enabled
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -359,6 +359,8 @@ en:
|
|||
placeholder: "type the message title here"
|
||||
|
||||
review:
|
||||
awaiting_approval: "Awaiting Approval"
|
||||
delete: "Delete"
|
||||
settings:
|
||||
saved: "Saved"
|
||||
save_changes: "Save Changes"
|
||||
|
|
|
@ -325,6 +325,7 @@ Discourse::Application.routes.draw do
|
|||
action_id: /[a-z\_]+/
|
||||
}
|
||||
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_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?
|
||||
end
|
||||
|
||||
def can_delete_reviewable_queued_post?(reviewable)
|
||||
reviewable.present? &&
|
||||
authenticated? &&
|
||||
reviewable.created_by_id == @user.id
|
||||
end
|
||||
|
||||
def can_see_group?(group)
|
||||
return false if group.blank?
|
||||
return true if group.visibility_level == Group.visibility_levels[:public]
|
||||
|
|
|
@ -6,8 +6,26 @@ require_dependency 'gaps'
|
|||
class TopicView
|
||||
MEGA_TOPIC_POSTS_COUNT = 10000
|
||||
|
||||
attr_reader :topic, :posts, :guardian, :filtered_posts, :chunk_size, :print, :message_bus_last_id
|
||||
attr_accessor :draft, :draft_key, :draft_sequence, :user_custom_fields, :post_custom_fields, :post_number
|
||||
attr_reader(
|
||||
: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
|
||||
1000
|
||||
|
@ -81,6 +99,9 @@ class TopicView
|
|||
|
||||
@draft_key = @topic.draft_key
|
||||
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
||||
|
||||
@queued_posts_enabled = NewPostManager.queue_enabled?
|
||||
@personal_message = @topic.private_message?
|
||||
end
|
||||
|
||||
def canonical_path
|
||||
|
@ -378,6 +399,13 @@ class TopicView
|
|||
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
|
||||
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
||||
end
|
||||
|
@ -390,6 +418,27 @@ class TopicView
|
|||
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
||||
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
|
||||
@link_counts ||= TopicLink.counts_for(@guardian, @topic, posts)
|
||||
end
|
||||
|
@ -493,6 +542,10 @@ class TopicView
|
|||
end
|
||||
end
|
||||
|
||||
def queued_posts_count
|
||||
ReviewableQueuedPost.viewable_by(@user).where(topic_id: @topic.id).pending.count
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_posts_set
|
||||
|
|
|
@ -803,6 +803,10 @@ describe PostsController do
|
|||
rp = ReviewableQueuedPost.find_by(created_by: user)
|
||||
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)
|
||||
rp.perform(mod, :approve)
|
||||
|
||||
|
|
|
@ -17,6 +17,11 @@ describe ReviewablesController do
|
|||
get "/review/settings.json"
|
||||
expect(response.code).to eq("403")
|
||||
end
|
||||
|
||||
it "denies deleting" do
|
||||
delete "/review/123"
|
||||
expect(response.code).to eq("403")
|
||||
end
|
||||
end
|
||||
|
||||
context "regular user" do
|
||||
|
@ -452,6 +457,32 @@ describe ReviewablesController do
|
|||
|
||||
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
|
||||
|
|
|
@ -3,7 +3,8 @@ require 'rails_helper'
|
|||
describe TopicViewSerializer do
|
||||
def serialize_topic(topic, 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
|
||||
|
||||
before do
|
||||
|
@ -53,7 +54,7 @@ describe TopicViewSerializer do
|
|||
it 'should include suggested topics' do
|
||||
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
|
||||
|
||||
|
@ -130,4 +131,84 @@ describe TopicViewSerializer do
|
|||
expect(json[:tags]).to eq([])
|
||||
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
|
||||
|
|
|
@ -255,6 +255,8 @@ QUnit.test("Posting on a different topic", async assert => {
|
|||
QUnit.test("Create an enqueued Reply", async assert => {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
assert.notOk(find(".pending-posts .reviewable-item").length);
|
||||
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
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");
|
||||
|
@ -270,6 +272,8 @@ QUnit.test("Create an enqueued Reply", async assert => {
|
|||
|
||||
await click(".modal-footer button");
|
||||
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 => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*jshint maxlen:10000000 */
|
||||
export default {
|
||||
"/t/280/1.json": {
|
||||
pending_posts: [],
|
||||
post_stream: {
|
||||
posts: [
|
||||
{
|
||||
|
|
|
@ -430,7 +430,14 @@ export default function() {
|
|||
}
|
||||
|
||||
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, {
|
||||
|
|
Loading…
Reference in New Issue