diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 05b7ce921ad..95d45de7b61 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,42 +1,42 @@ class CommentsController < ApplicationController before_action :load_commentable, - only: [:index, :new, :create, :edit, :update, :show_comments, + only: [:index, :new, :draft, :create, :preview, :edit, :update, :show_comments, :hide_comments, :add_comment_reply, :cancel_comment_reply, :delete_comment, :cancel_comment_delete, :unreviewed, :review_all] - before_action :check_user_status, only: [:new, :create, :edit, :update, :destroy] + before_action :check_user_status, only: [:new, :draft, :create, :preview, :edit, :update, :destroy] before_action :load_comment, only: [:show, :edit, :update, :delete_comment, :destroy, :cancel_comment_edit, :cancel_comment_delete, :review, :approve, :reject, :freeze, :unfreeze, :hide, :unhide] before_action :check_visibility, only: [:show] before_action :check_if_restricted before_action :check_tag_wrangler_access before_action :check_parent_visible before_action :check_modify_parent, - only: [:new, :create, :edit, :update, :add_comment_reply, + only: [:new, :draft, :create, :preview, :edit, :update, :add_comment_reply, :cancel_comment_reply, :cancel_comment_edit] before_action :check_pseud_ownership, only: [:create, :update] before_action :check_ownership, only: [:edit, :update, :cancel_comment_edit] before_action :check_permission_to_edit, only: [:edit, :update] before_action :check_permission_to_delete, only: [:delete_comment, :destroy] - before_action :check_guest_comment_admin_setting, only: [:new, :create, :add_comment_reply] - before_action :check_parent_comment_permissions, only: [:new, :create, :add_comment_reply] + before_action :check_guest_comment_admin_setting, only: [:new, :draft, :create, :preview, :add_comment_reply] + before_action :check_parent_comment_permissions, only: [:new, :draft, :create, :preview, :add_comment_reply] before_action :check_unreviewed, only: [:add_comment_reply] - before_action :check_frozen, only: [:new, :create, :add_comment_reply] - before_action :check_hidden_by_admin, only: [:new, :create, :add_comment_reply] - before_action :check_not_replying_to_spam, only: [:new, :create, :add_comment_reply] - before_action :check_guest_replies_preference, only: [:new, :create, :add_comment_reply] + before_action :check_frozen, only: [:new, :draft, :create, :preview, :add_comment_reply] + before_action :check_hidden_by_admin, only: [:new, :draft, :create, :preview, :add_comment_reply] + before_action :check_not_replying_to_spam, only: [:new, :draft, :create, :preview, :add_comment_reply] + before_action :check_guest_replies_preference, only: [:new, :draft, :create, :preview, :add_comment_reply] before_action :check_permission_to_review, only: [:unreviewed] before_action :check_permission_to_access_single_unreviewed, only: [:show] before_action :check_permission_to_moderate, only: [:approve, :reject] before_action :check_permission_to_modify_frozen_status, only: [:freeze, :unfreeze] before_action :check_permission_to_modify_hidden_status, only: [:hide, :unhide] before_action :check_guest_email_is_from_suspended_or_banned_user, only: [:create] - before_action :admin_logout_required, only: [:new, :create, :add_comment_reply] + before_action :admin_logout_required, only: [:new, :draft, :create, :add_comment_reply] before_action :set_page_subtitle, only: [:index, :new, :show, :unreviewed] include WorksHelper include BlockHelper - before_action :check_blocked, only: [:new, :create, :add_comment_reply, :edit, :update] + before_action :check_blocked, only: [:new, :create, :preview, :add_comment_reply, :edit, :update] def check_blocked parent = find_parent @@ -95,7 +95,7 @@ def check_pseud_ownership return unless params[:comment][:pseud_id] pseud = Pseud.find(params[:comment][:pseud_id]) return if pseud && current_user && current_user.pseuds.include?(pseud) - flash[:error] = ts("You can't comment with that pseud.") + flash[:error] = t("comments.check_pseud_ownership.error") redirect_to root_path end @@ -113,12 +113,12 @@ def check_modify_parent parent = find_parent # No one can create or update comments on something hidden by an admin. if parent.respond_to?(:hidden_by_admin) && parent.hidden_by_admin - flash[:error] = ts("Sorry, you can't add or edit comments on a hidden work.") + flash[:error] = t("comments.commentable.permissions.work.hidden") redirect_to work_path(parent) end # No one can create or update comments on unrevealed works. if parent.respond_to?(:in_unrevealed_collection) && parent.in_unrevealed_collection - flash[:error] = ts("Sorry, you can't add or edit comments on an unrevealed work.") + flash[:error] = t("comments.commentable.permissions.work.unrevealed") redirect_to work_path(parent) end @@ -190,7 +190,7 @@ def check_guest_replies_preference def check_unreviewed return unless @commentable.respond_to?(:unreviewed?) && @commentable.unreviewed? - flash[:error] = ts("Sorry, you cannot reply to an unapproved comment.") + flash[:error] = t("comments.check_unreviewed.error") redirect_to logged_in? ? root_path : new_user_session_path(return_to: request.fullpath) end @@ -218,7 +218,7 @@ def check_not_replying_to_spam def check_permission_to_review parent = find_parent return if logged_in_as_admin? || current_user_owns?(parent) - flash[:error] = ts("Sorry, you don't have permission to see those unreviewed comments.") + flash[:error] = t("comments.check_permission_to_review.error") redirect_to logged_in? ? root_path : new_user_session_path(return_to: request.fullpath) end @@ -226,14 +226,14 @@ def check_permission_to_access_single_unreviewed return unless @comment.unreviewed? parent = find_parent return if logged_in_as_admin? || current_user_owns?(parent) || current_user_owns?(@comment) - flash[:error] = ts("Sorry, that comment is currently in moderation.") + flash[:error] = t("comments.check_permission_to_access_single_unreviewed.error") redirect_to logged_in? ? root_path : new_user_session_path(return_to: request.fullpath) end def check_permission_to_moderate return if logged_in_as_admin? || current_user_owns?(find_parent) - flash[:error] = ts("Sorry, you don't have permission to moderate that comment.") + flash[:error] = t("comments.check_permission_to_moderate.error") redirect_to(logged_in? ? root_path : new_user_session_path(return_to: comment_path(@comment))) end @@ -254,7 +254,7 @@ def check_permission_to_edit flash[:error] = t("comments.check_permission_to_edit.error.frozen") redirect_back_or_to @comment elsif !@comment.count_all_comments.zero? - flash[:error] = ts("Comments with replies cannot be edited") + flash[:error] = t("comments.check_permission_to_edit.error.has_replies") redirect_back_or_to @comment end end @@ -362,27 +362,51 @@ def show # GET /comments/new def new if @commentable.nil? - flash[:error] = ts("What did you want to comment on?") + flash[:error] = t("comments.new.missing_commentable") redirect_back_or_to root_path else - @comment = Comment.new + @comment = build_comment_for_form @controller_name = params[:controller_name] if params[:controller_name] @name = case @commentable.class.name - when /Work/ + when /Work/, /AdminPost/ @commentable.title when /Chapter/ @commentable.work.title when /Tag/ @commentable.name - when /AdminPost/ + when /Comment/ + t("comments.new.previous_comment") + else + @commentable.class.name + end + end + end + + # POST /comments/draft + def draft + if @commentable.nil? + flash[:error] = t("comments.new.missing_commentable") + redirect_back_or_to root_path + else + @comment = build_comment_for_form + @controller_name = params[:controller_name] if params[:controller_name] + @name = + case @commentable.class.name + when /Work/, /AdminPost/ @commentable.title + when /Chapter/ + @commentable.work.title + when /Tag/ + @commentable.name when /Comment/ - ts("Previous Comment") + t("comments.new.previous_comment") else @commentable.class.name end end + + render :new end # GET /comments/1/edit @@ -393,57 +417,108 @@ def edit end end + # GET /comments/preview + def preview + if @commentable.nil? + flash[:error] = t(".missing_commentable") + redirect_back_or_to root_path + return + end + + @comment = Comment.new(comment_params) + @comment.commentable = Comment.commentable_object(@commentable) + + @comment.set_parent_and_unreviewed + + unless @comment.valid? + render :new, locals: { show_errors: true } + return + end + + @preview_mode = true + @form_state = { comment_content: @comment.comment_content } + @form_state[:pseud_id] = @comment.pseud_id if @comment.pseud_id + @form_state[:name] = @comment.name if @comment.name + @form_state[:email] = @comment.email if @comment.email + @form_state[:view_full_work] = params[:view_full_work] if params[:view_full_work] + @form_state[:page] = params[:page] if params[:page] + @form_state[:controller_name] = params[:controller_name] if params[:controller_name] + + case @commentable + when Work + @form_state[:work_id] = @commentable.id + when Chapter + @form_state[:chapter_id] = @commentable.id + when AdminPost + @form_state[:admin_post_id] = @commentable.id + when Tag + @form_state[:tag_id] = @commentable.name + when Comment + @form_state[:comment_id] = @commentable.id + end + + @form_state[:filters] = filter_params.to_h if controller_name == "inbox" && params[:filters] + + render :preview + end + # POST /comments # POST /comments.xml def create - if @commentable.nil? - flash[:error] = ts("What did you want to comment on?") + if params[:preview_button] + preview + return + end + + if @commentable.blank? + flash[:error] = t("comments.create.missing_commentable") redirect_back_or_to root_path - else - @comment = Comment.new(comment_params) - @comment.ip_address = request.remote_ip - @comment.user_agent = request.env["HTTP_USER_AGENT"]&.to(499) - @comment.cloudflare_bot_score = request.env["HTTP_CF_BOT_SCORE"] - @comment.cloudflare_ja3_hash = request.env["HTTP_CF_JA3_HASH"] - @comment.cloudflare_ja4 = request.env["HTTP_CF_JA4"] - @comment.commentable = Comment.commentable_object(@commentable) - @controller_name = params[:controller_name] - - # First, try saving the comment - if @comment.save - flash[:comment_notice] = if @comment.unreviewed? - # i18n-tasks-use t("comments.create.success.moderated.admin_post") - # i18n-tasks-use t("comments.create.success.moderated.work") - t("comments.create.success.moderated.#{@comment.ultimate_parent.model_name.i18n_key}") - else - t("comments.create.success.not_moderated") - end - respond_to do |format| - format.html do - if request.referer&.match(/inbox/) - redirect_to user_inbox_path(current_user, filters: filter_params, page: params[:page]) - elsif request.referer&.match(/new/) || (@comment.unreviewed? && current_user) - # If the referer is the new comment page, go to the comment's page - # instead of reloading the full work. - # If the comment is unreviewed and commenter is logged in, take - # them to the comment's page so they can access the edit and - # delete options for the comment, since unreviewed comments don't - # appear on the commentable. - redirect_to comment_path(@comment) - elsif request.referer == root_url - # replying on the homepage - redirect_to root_path - elsif @comment.unreviewed? - redirect_to_all_comments(@commentable) - else - redirect_to_comment(@comment, { view_full_work: (params[:view_full_work] == "true"), page: params[:page] }) - end + return + end + + @comment = Comment.new(comment_params) + @comment.ip_address = request.remote_ip + @comment.user_agent = request.env["HTTP_USER_AGENT"]&.to(499) + @comment.cloudflare_bot_score = request.env["HTTP_CF_BOT_SCORE"] + @comment.cloudflare_ja3_hash = request.env["HTTP_CF_JA3_HASH"] + @comment.cloudflare_ja4 = request.env["HTTP_CF_JA4"] + @comment.commentable = Comment.commentable_object(@commentable) + @controller_name = params[:controller_name] + + # First, try saving the comment + if @comment.save + flash[:comment_notice] = if @comment.unreviewed? + # i18n-tasks-use t("comments.create.success.moderated.admin_post") + # i18n-tasks-use t("comments.create.success.moderated.work") + t("comments.create.success.moderated.#{@comment.ultimate_parent.model_name.i18n_key}") + else + t("comments.create.success.not_moderated") + end + respond_to do |format| + format.html do + if request.referer&.match(/inbox/) + redirect_to user_inbox_path(current_user, filters: filter_params, page: params[:page]) + elsif request.referer&.match(/new/) || (@comment.unreviewed? && current_user) + # If the referer is the new comment page, go to the comment's page + # instead of reloading the full work. + # If the comment is unreviewed and commenter is logged in, take + # them to the comment's page so they can access the edit and + # delete options for the comment, since unreviewed comments don't + # appear on the commentable. + redirect_to comment_path(@comment) + elsif request.referer == root_url + # replying on the homepage + redirect_to root_path + elsif @comment.unreviewed? + redirect_to_all_comments(@commentable) + else + redirect_to_comment(@comment, { view_full_work: (params[:view_full_work] == "true"), page: params[:page] }) end end - else - flash[:error] = ts("Couldn't save comment!") - render action: "new" end + else + flash[:error] = t("comments.create.error") + render action: "new" end end @@ -452,7 +527,7 @@ def create def update updated_comment_params = comment_params.merge(edited_at: Time.current) if @comment.update(updated_comment_params) - flash[:comment_notice] = ts('Comment was successfully updated.') + flash[:comment_notice] = t("comments.update.success") respond_to do |format| format.html do redirect_to comment_path(@comment) and return if @comment.unreviewed? @@ -476,17 +551,17 @@ def destroy if !@comment.destroy_or_mark_deleted # something went wrong? - flash[:comment_error] = ts("We couldn't delete that comment.") + flash[:comment_error] = t("comments.destroy.error") redirect_to_comment(@comment) elsif unreviewed # go back to the rest of the unreviewed comments - flash[:notice] = ts("Comment deleted.") + flash[:notice] = t("comments.destroy.success") redirect_back_or_to unreviewed_work_comments_path(@comment.commentable) elsif parent_comment - flash[:comment_notice] = ts("Comment deleted.") + flash[:comment_notice] = t("comments.destroy.success") redirect_to_comment(parent_comment) else - flash[:comment_notice] = ts("Comment deleted.") + flash[:comment_notice] = t("comments.destroy.success") redirect_to_all_comments(parent, {show_comments: true}) end end @@ -503,7 +578,7 @@ def review @comment.toggle!(:unreviewed) # mark associated inbox comments as read InboxComment.where(user_id: current_user.id, feedback_comment_id: @comment.id).update_all(read: true) unless logged_in_as_admin? - flash[:notice] = ts("Comment approved.") + flash[:notice] = t("comments.review.success") respond_to do |format| format.html do if params[:approved_from] == "inbox" @@ -764,6 +839,20 @@ def permission_to_modify_frozen_status private + def build_comment_for_form + if params[:comment_content].present? || params[:comment].present? + comment_attrs = { + comment_content: params[:comment_content] || params.dig(:comment, :comment_content), + pseud_id: params[:pseud_id] || params.dig(:comment, :pseud_id), + name: params[:name] || params.dig(:comment, :name), + email: params[:email] || params.dig(:comment, :email) + } + Comment.new(comment_attrs.compact) + else + Comment.new + end + end + def comment_params params.require(:comment).permit( :pseud_id, :comment_content, :name, :email, :edited_at diff --git a/app/views/comments/_comment_form.html.erb b/app/views/comments/_comment_form.html.erb index cc4660bcd8e..a7b22eb55cd 100644 --- a/app/views/comments/_comment_form.html.erb +++ b/app/views/comments/_comment_form.html.erb @@ -102,6 +102,7 @@ maximum_length: ArchiveConfig.COMMENT_MAX, tooLongMessage: t(".comment_too_long", count: ArchiveConfig.COMMENT_MAX) %>

+ <%= f.submit t(".preview_button"), name: "preview_button", id: "comment_preview_for_#{commentable.id}" %> <%= f.submit button_name, id: "comment_submit_for_#{commentable.id}", data: { disable_with: t(".processing_message") } %> <% if controller.controller_name == 'inbox' %> <%= t(".cancel_action") %> diff --git a/app/views/comments/preview.html.erb b/app/views/comments/preview.html.erb new file mode 100644 index 00000000000..771ade1af25 --- /dev/null +++ b/app/views/comments/preview.html.erb @@ -0,0 +1,76 @@ +

<%= t(".page_heading") %>

+

<%= t(".commenting_on_html", commentable_link: link_to_comment_ultimate_parent(@comment)) %>

+ + +
+
    +
  1. + +
    + <% if @comment.pseud %> + <% if @comment.by_anonymous_creator? %> + + <% else %> + <%= icon_display(@comment.pseud.user, @comment.pseud) %> + <% end %> + <% else %> + + <% end %> +
    +
    + <%= raw sanitize_field(@comment, :comment_content) %> +
    +
  2. +
+
+ + + + + diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml index 880ebc12684..059aceb7e74 100644 --- a/config/locales/controllers/en.yml +++ b/config/locales/controllers/en.yml @@ -159,9 +159,6 @@ en: draft: Sorry, you can't comment on a draft. check_not_replying_to_spam: error: Sorry, you can't reply to a comment that has been marked as spam. - check_permission_to_edit: - error: - frozen: Frozen comments cannot be edited. create: success: moderated: diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index 630a3291f93..f37551cbc4a 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -847,6 +847,20 @@ en: tags: Tags works: Works (%{count}) comments: + check_permission_to_access_single_unreviewed: + error: Sorry, that comment is currently in moderation. + check_permission_to_edit: + error: + frozen: This comment is frozen. + has_replies: Comments with replies cannot be edited + check_permission_to_moderate: + error: Sorry, you don't have permission to moderate that comment. + check_permission_to_review: + error: Sorry, you don't have permission to see those unreviewed comments. + check_pseud_ownership: + error: You can't comment with that pseud. + check_unreviewed: + error: Sorry, you cannot reply to an unapproved comment. comment_actions: approve: Approve comment: Comment @@ -874,6 +888,7 @@ en: comment: Comment note: Note legend: Post Comment + preview_button: Preview processing_message: Please wait... commentable: actions: @@ -915,11 +930,29 @@ en: confirm_delete: confirm_button: Yes, delete! delete_confirmation: Are you sure you want to delete this comment? + create: + error: Couldn't save comment! + missing_commentable: What did you want to comment on? + destroy: + error: We couldn't delete that comment. + success: Comment deleted. edit: back: Back page_heading: Editing comment show: Show update: Update + new: + missing_commentable: What did you want to comment on? + previous_comment: Previous Comment + preview: + anonymous_creator: Anonymous Creator + back_to_edit: Back to Edit + commenting_on_html: 'Commenting on: %{commentable_link}' + missing_commentable: What did you want to comment on? + page_heading: Preview Comment + post_comment: Post Comment + review: + success: Comment approved. show: comment_on_html: Comment on %{commentable_link} single_comment: @@ -931,6 +964,8 @@ en: admin_post: Please note that comments cannot be unapproved once you have approved them. After you delete any comments you do not wish to appear on the news post, you can approve all that remain. work: Please note that comments cannot be unapproved once you have reviewed them. After you delete any comments you do not wish to appear on your work, you can approve all that remain. page_heading_html: Unreviewed Comments on %{commentable_link} + update: + success: Comment was successfully updated. downloads: download_afterword: afterword: Afterword diff --git a/config/routes.rb b/config/routes.rb index 5c5c951551f..f661939f4e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -578,6 +578,8 @@ put :unhide end collection do + post :preview + post :draft get :hide_comments get :show_comments get :add_comment_reply diff --git a/features/comments_and_kudos/add_comment.feature b/features/comments_and_kudos/add_comment.feature index d90b2ef166a..c5bb25f4fab 100644 --- a/features/comments_and_kudos/add_comment.feature +++ b/features/comments_and_kudos/add_comment.feature @@ -457,3 +457,80 @@ Scenario: Guest comments with an email from a banned or suspended user should be And the email to "creator" should contain "edited their reply to your comment on" And the email to "creator" should contain "Go to the thread starting from this comment" And the email to "creator" should be translated + +Scenario: Comment preview + Given the work "The One Where Neal is Awesome" + When I am logged in as "commenter" + And I view the work "The One Where Neal is Awesome" + And I fill in "Comment" with "This is my test comment with bold text" + And I press "Preview" + Then I should see "Preview Comment" + And I should see "Commenting on: The One Where Neal is Awesome" + And I should see "This is my test comment with bold text" + And I should see "commenter" in the comment byline + And I should see a button "Back to Edit" + And I should see a button "Post Comment" + +Scenario: Comment preview persists content when going back to edit + Given the work "The One Where Neal is Awesome" + When I am logged in as "commenter" + And I view the work "The One Where Neal is Awesome" + And I fill in "Comment" with "Initial comment content" + And I press "Preview" + Then I should see "Preview Comment" + When I press "Back to Edit" + Then I should see "Initial comment content" in the comment field + And I should see the "Preview" button + When I fill in "Comment" with "Initial comment content with more text" + And I press "Preview" + Then I should see "Initial comment content with more text" + When I press "Back to Edit" + Then I should see "Initial comment content with more text" in the comment field + +Scenario: Comment preview and post + Given the work "The One Where Neal is Awesome" + When I am logged in as "commenter" + And I view the work "The One Where Neal is Awesome" + And I fill in "Comment" with "I really enjoyed this!" + And I press "Preview" + Then I should see "Preview Comment" + And I should see "I really enjoyed this!" + When I press "Post Comment" + Then I should see "Comment created!" + And I should see "I really enjoyed this!" within ".odd" + +Scenario: Comment preview shows sanitized HTML + Given the work "The One Where Neal is Awesome" + When I am logged in as "commenter" + And I view the work "The One Where Neal is Awesome" + And I fill in "Comment" with "Safe HTML: bold, italic,

paragraph

" + And I press "Preview" + Then I should see "Preview Comment" + And I should see "bold" + And I should see "italic" + And I should see "paragraph" + +Scenario: Guest comment preview + Given the work "The One Where Neal is Awesome" by "creator" with guest comments enabled + When I view the work "The One Where Neal is Awesome" + And I fill in "Guest name" with "Guest User" + And I fill in "Guest email" with "guest@example.com" + And I fill in "Comment" with "Great work!" + And I press "Preview" + Then I should see "Preview Comment" + And I should see "Guest User" in the comment byline + And I should see "Great work!" + And I should see a button "Back to Edit" + When I press "Back to Edit" + Then I should see "Great work!" in the comment field + And I should see "Guest User" in the guest name field + +Scenario: Comment preview for anonymous work + Given there is a work "The One Where Neal is Awesome" in an anonymous collection "Anonymous Hugs" + When I am logged in as the author of "The One Where Neal is Awesome" + And I view the work "The One Where Neal is Awesome" + And I fill in "Comment" with "Anonymous work comment" + And I press "Preview" + Then I should see "Preview Comment" + And I should see "Anonymous Creator" in the comment byline + And I should see "Anonymous work comment" diff --git a/features/step_definitions/comment_steps.rb b/features/step_definitions/comment_steps.rb index 22d1cc413a1..ec4b4fe4cfc 100644 --- a/features/step_definitions/comment_steps.rb +++ b/features/step_definitions/comment_steps.rb @@ -410,3 +410,23 @@ When "I reply on a new page" do visit find(:link, "Reply")["href"] end + +Then "I should see {string} in the comment byline" do |text| + step %{I should see "#{text}" within ".byline"} +end + +Then "I should see a button {string}" do |text| + expect(page).to have_button(text) +end + +Then "I should see {string} in the comment field" do |text| + expect(find('textarea[name="comment[comment_content]"]').value).to include(text) +end + +Then "I should see the {string} button" do |text| + expect(page).to have_button(text) +end + +Then "I should see {string} in the guest name field" do |text| + expect(find('input[name="comment[name]"]').value).to eq(text) +end diff --git a/public/stylesheets/site/2.0/08-actions.css b/public/stylesheets/site/2.0/08-actions.css index d9a2cabd816..9473824f871 100644 --- a/public/stylesheets/site/2.0/08-actions.css +++ b/public/stylesheets/site/2.0/08-actions.css @@ -60,7 +60,7 @@ input[type="submit"] { white-space: normal; } -p.submit, input.submit, dd.submit { +p.submit, input.submit, dd.submit, ul.actions.right { text-align: right; }