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:
Robin Ward 2019-04-12 09:55:27 -04:00
parent 1760069419
commit a5d9afe397
26 changed files with 503 additions and 131 deletions

View File

@ -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");

View File

@ -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);
},

View File

@ -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")}}

View File

@ -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;
}
}
}
}

View File

@ -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)

View File

@ -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
#

View File

@ -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
#

View File

@ -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
#

View File

@ -86,6 +86,7 @@ end
# meta_topic_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# reason :string
#
# Indexes
#

View File

@ -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
#

View File

@ -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)

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -0,0 +1,5 @@
class AddCreatedByIndexToReviewables < ActiveRecord::Migration[5.2]
def change
add_index :reviewables, [:topic_id, :status, :created_by_id]
end
end

View File

@ -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]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 => {

View File

@ -1,6 +1,7 @@
/*jshint maxlen:10000000 */
export default {
"/t/280/1.json": {
pending_posts: [],
post_stream: {
posts: [
{

View File

@ -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, {