diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index 0641f1120ee..be239a81e73 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -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");
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index a51c2dda9a6..7c0ba8b6e2f 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -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);
},
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index 9b09579936f..ae8c8e58d24 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -211,10 +211,41 @@
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
{{#if loadedAllPosts}}
- {{#if model.pending_posts_count}}
+ {{#if model.pending_posts}}
+
- {{{i18n "review.topic_has_pending" count=model.pending_posts_count}}}
+ {{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss
index b54bca6097b..dd9fcb5e272 100644
--- a/app/assets/stylesheets/common/base/topic.scss
+++ b/app/assets/stylesheets/common/base/topic.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb
index f91dcbf6cf2..3551c9ddecf 100644
--- a/app/controllers/reviewables_controller.rb
+++ b/app/controllers/reviewables_controller.rb
@@ -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)
diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb
index 5debd5ba4cf..5e9969a00d8 100644
--- a/app/models/reviewable.rb
+++ b/app/models/reviewable.rb
@@ -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
#
diff --git a/app/models/reviewable_flagged_post.rb b/app/models/reviewable_flagged_post.rb
index 67ae263386a..be98034fb56 100644
--- a/app/models/reviewable_flagged_post.rb
+++ b/app/models/reviewable_flagged_post.rb
@@ -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
#
diff --git a/app/models/reviewable_queued_post.rb b/app/models/reviewable_queued_post.rb
index c60fd5d9358..8f8bf123cb8 100644
--- a/app/models/reviewable_queued_post.rb
+++ b/app/models/reviewable_queued_post.rb
@@ -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
#
diff --git a/app/models/reviewable_score.rb b/app/models/reviewable_score.rb
index 057afb5612b..144b5ac9d33 100644
--- a/app/models/reviewable_score.rb
+++ b/app/models/reviewable_score.rb
@@ -86,6 +86,7 @@ end
# meta_topic_id :integer
# created_at :datetime not null
# updated_at :datetime not null
+# reason :string
#
# Indexes
#
diff --git a/app/models/reviewable_user.rb b/app/models/reviewable_user.rb
index 40185a35dcc..8feefedf612 100644
--- a/app/models/reviewable_user.rb
+++ b/app/models/reviewable_user.rb
@@ -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
#
diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb
index a7be7fa00c3..43c41d563e4 100644
--- a/app/models/topic_link.rb
+++ b/app/models/topic_link.rb
@@ -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)
diff --git a/app/serializers/new_post_result_serializer.rb b/app/serializers/new_post_result_serializer.rb
index 068f3a9c9d7..39da1ecfe25 100644
--- a/app/serializers/new_post_result_serializer.rb
+++ b/app/serializers/new_post_result_serializer.rb
@@ -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
diff --git a/app/serializers/topic_pending_post_serializer.rb b/app/serializers/topic_pending_post_serializer.rb
new file mode 100644
index 00000000000..568f9b6eab4
--- /dev/null
+++ b/app/serializers/topic_pending_post_serializer.rb
@@ -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
diff --git a/app/serializers/topic_view_details_serializer.rb b/app/serializers/topic_view_details_serializer.rb
new file mode 100644
index 00000000000..8ad7a74e9a9
--- /dev/null
+++ b/app/serializers/topic_view_details_serializer.rb
@@ -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
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index 8a7105bb91b..48ffd7d4bca 100644
--- a/app/serializers/topic_view_serializer.rb
+++ b/app/serializers/topic_view_serializer.rb
@@ -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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e48bbae6be0..9084946b016 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -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"
diff --git a/config/routes.rb b/config/routes.rb
index 9cc0354d66a..d02121714f6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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"
diff --git a/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb b/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb
new file mode 100644
index 00000000000..243a0a1f3c1
--- /dev/null
+++ b/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb
@@ -0,0 +1,5 @@
+class AddCreatedByIndexToReviewables < ActiveRecord::Migration[5.2]
+ def change
+ add_index :reviewables, [:topic_id, :status, :created_by_id]
+ end
+end
diff --git a/lib/guardian.rb b/lib/guardian.rb
index 5207989df1a..558de2cfbed 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -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]
diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index 66da453ba05..af81eb703f2 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -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
diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb
index 7e5e65557c6..5a0ebd2c21e 100644
--- a/spec/requests/posts_controller_spec.rb
+++ b/spec/requests/posts_controller_spec.rb
@@ -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)
diff --git a/spec/requests/reviewables_controller_spec.rb b/spec/requests/reviewables_controller_spec.rb
index bf061003025..f88a872d673 100644
--- a/spec/requests/reviewables_controller_spec.rb
+++ b/spec/requests/reviewables_controller_spec.rb
@@ -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
diff --git a/spec/serializers/topic_view_serializer_spec.rb b/spec/serializers/topic_view_serializer_spec.rb
index 5b50522b890..645750ffb14 100644
--- a/spec/serializers/topic_view_serializer_spec.rb
+++ b/spec/serializers/topic_view_serializer_spec.rb
@@ -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
diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6
index e7e801e22d6..c93d166adc4 100644
--- a/test/javascripts/acceptance/composer-test.js.es6
+++ b/test/javascripts/acceptance/composer-test.js.es6
@@ -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 => {
diff --git a/test/javascripts/fixtures/topic.js.es6 b/test/javascripts/fixtures/topic.js.es6
index f4b1459d7a8..7467e71a1e9 100644
--- a/test/javascripts/fixtures/topic.js.es6
+++ b/test/javascripts/fixtures/topic.js.es6
@@ -1,6 +1,7 @@
/*jshint maxlen:10000000 */
export default {
"/t/280/1.json": {
+ pending_posts: [],
post_stream: {
posts: [
{
diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6
index ed46639feef..a7313a8a369 100644
--- a/test/javascripts/helpers/create-pretender.js.es6
+++ b/test/javascripts/helpers/create-pretender.js.es6
@@ -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, {