From 1de91b9f57898c69639d2e3f8a7fcec5403d1ef4 Mon Sep 17 00:00:00 2001 From: Marcus Low Date: Sat, 16 May 2026 21:30:27 -0700 Subject: [PATCH 01/12] AO3-6960 Admin post drafts and previews --- app/controllers/admin_posts_controller.rb | 109 ++-- app/helpers/admin_post_helper.rb | 5 +- app/models/admin_post.rb | 26 +- app/policies/admin_post_policy.rb | 22 +- app/views/admin/_admin_nav.html.erb | 7 + app/views/admin/_header.html.erb | 5 + app/views/admin_posts/_admin_index.html.erb | 16 +- app/views/admin_posts/_admin_post.html.erb | 13 +- .../admin_posts/_admin_post_blurb.html.erb | 25 + .../admin_posts/_admin_post_form.html.erb | 8 +- app/views/admin_posts/_filters.html.erb | 2 +- .../admin_posts/_posting_fieldset.html.erb | 26 + app/views/admin_posts/drafts.html.erb | 21 + app/views/admin_posts/index.html.erb | 2 +- app/views/admin_posts/index.rss.builder | 2 +- app/views/admin_posts/preview.html.erb | 21 + app/views/admin_posts/show.html.erb | 18 +- app/views/comments/_commentable.html.erb | 10 +- app/views/home/_news_module.html.erb | 2 +- config/locales/controllers/en.yml | 8 + config/locales/views/en.yml | 29 ++ config/routes.rb | 5 + ...tus_and_publication_date_to_admin_posts.rb | 20 + factories/admin_post.rb | 7 + features/admins/admin_post_draft_news.feature | 162 ++++++ features/admins/admin_post_news.feature | 19 + features/step_definitions/admin_steps.rb | 14 + features/support/paths.rb | 2 + .../admin_posts_controller_spec.rb | 488 ++++++++++-------- spec/helpers/admin_post_helper_spec.rb | 41 +- spec/models/admin_post_spec.rb | 26 + .../shared_examples/access_shared_examples.rb | 20 + 32 files changed, 870 insertions(+), 311 deletions(-) create mode 100644 app/views/admin_posts/_admin_post_blurb.html.erb create mode 100644 app/views/admin_posts/_posting_fieldset.html.erb create mode 100644 app/views/admin_posts/drafts.html.erb create mode 100755 app/views/admin_posts/preview.html.erb create mode 100644 db/migrate/20260509172710_add_posted_status_and_publication_date_to_admin_posts.rb create mode 100644 features/admins/admin_post_draft_news.feature diff --git a/app/controllers/admin_posts_controller.rb b/app/controllers/admin_posts_controller.rb index 426c71b3d3e..68911673b2a 100644 --- a/app/controllers/admin_posts_controller.rb +++ b/app/controllers/admin_posts_controller.rb @@ -2,36 +2,37 @@ class AdminPostsController < Admin::BaseController before_action :admin_only, except: [:index, :show] before_action :load_languages, except: [:show, :destroy] + before_action :load_admin_posts, only: [:index, :drafts] # GET /admin_posts def index - if params[:tag] - @tag = AdminPostTag.find_by(id: params[:tag]) - if @tag - @admin_posts = @tag.admin_posts - end - end - @admin_posts ||= AdminPost - if params[:language_id].present? && (@language = Language.find_by(short: params[:language_id])) - @admin_posts = @admin_posts.where(language_id: @language.id) - @tags = AdminPostTag.distinct.joins(:admin_posts).where(admin_posts: { language_id: @language.id }).order(:name) - else - @admin_posts = @admin_posts.non_translated - @tags = AdminPostTag.order(:name) - end - @admin_posts = @admin_posts.order('created_at DESC').page(params[:page]) + @page_subtitle = t(".page_title") + @admin_posts = @admin_posts.posted.order(published_at: :desc).page(params[:page]) + end + + # GET /admin_posts/drafts + def drafts + authorize AdminPost + + @page_subtitle = t(".page_title") + @pagy, @admin_posts = pagy(@admin_posts.unposted.order(created_at: :desc)) end # GET /admin_posts/1 def show + @admin_post = AdminPost.find(params[:id]) + authorize(@admin_post) unless @admin_post.posted? + admin_posts = AdminPost.non_translated - @admin_post = AdminPost.find_by(id: params[:id]) - unless @admin_post - raise ActiveRecord::RecordNotFound, "Couldn't find admin post '#{params[:id]}'" + if @admin_post.posted? + @admin_posts = admin_posts.posted.order(published_at: :desc).limit(8) + @previous_admin_post = admin_posts.posted.order(published_at: :desc).where("published_at < ?", @admin_post.published_at).first + @next_admin_post = admin_posts.posted.order(published_at: :asc).where("published_at > ?", @admin_post.published_at).first + else + @admin_posts = admin_posts.unposted.order(created_at: :desc).limit(8) + @previous_admin_post = admin_posts.unposted.order(created_at: :desc).where("created_at < ?", @admin_post.created_at).first + @next_admin_post = admin_posts.unposted.order(created_at: :asc).where("created_at > ?", @admin_post.created_at).first end - @admin_posts = admin_posts.order('created_at DESC').limit(8) - @previous_admin_post = admin_posts.order('created_at DESC').where('created_at < ?', @admin_post.created_at).first - @next_admin_post = admin_posts.order('created_at ASC').where('created_at > ?', @admin_post.created_at).first @page_subtitle = @admin_post.title.html_safe respond_to do |format| format.html # show.html.erb @@ -39,6 +40,13 @@ def show end end + # GET /admin_posts/1/preview + def preview + @preview_mode = true + @admin_post = AdminPost.find(params[:id]) + authorize(@admin_post) + end + # GET /admin_posts/new # GET /admin_posts/new.xml def new @@ -55,10 +63,15 @@ def edit # POST /admin_posts def create @admin_post = AdminPost.new(admin_post_params) + @admin_post.posted = true if params[:post_button] && !@admin_post&.translated_post&.draft? + authorize @admin_post - if @admin_post.save - flash[:notice] = ts("Admin Post was successfully created.") - redirect_to(@admin_post) + if params[:preview_button] + @preview_mode = true + render action: "preview" + elsif !params[:edit_button] && @admin_post.save + flash[:notice] = t(".success") + redirect_to(admin_post_path(@admin_post)) else render action: "new" end @@ -66,13 +79,37 @@ def create # PUT /admin_posts/1 def update + @admin_post = AdminPost.find(params[:id]) + @admin_post.attributes = admin_post_params + @admin_post.posted = true if params[:post_button] && !@admin_post&.translated_post&.draft? + authorize @admin_post + + if !params[:edit_button] && @admin_post.valid? + if params[:preview_button] + @preview_mode = true + render :preview and return + elsif @admin_post.save + flash[:notice] = t(".success") + redirect_to(@admin_post) and return + end + end + + render action: "edit" + end + + # PUT /admin_posts/1/post + def post @admin_post = AdminPost.find(params[:id]) authorize @admin_post - if @admin_post.update(admin_post_params) - flash[:notice] = ts("Admin Post was successfully updated.") - redirect_to(@admin_post) + + @admin_post.posted = true + + if @admin_post.save + flash[:notice] = t(".success") + redirect_to @admin_post else - render action: "edit" + flash[:error] = t(".error") + redirect_to(edit_admin_post_path(@admin_post)) end end @@ -81,7 +118,8 @@ def destroy @admin_post = AdminPost.find(params[:id]) authorize @admin_post @admin_post.destroy - redirect_to(admin_posts_path) + + redirect_to(@admin_post.posted? ? admin_posts_path : drafts_admin_posts_path) end protected @@ -90,6 +128,19 @@ def load_languages @news_languages = Language.where(id: Locale.all.map(&:language_id)).default_order end + def load_admin_posts + @tag = AdminPostTag.find_by(id: params[:tag]) if params[:tag] + @admin_posts = @tag&.admin_posts || AdminPost + + if params[:language_id].present? && (@language = Language.find_by(short: params[:language_id])) + @admin_posts = @admin_posts.where(language_id: @language.id) + @tags = AdminPostTag.distinct.joins(:admin_posts).where(admin_posts: { language_id: @language.id }).order(:name) + else + @admin_posts = @admin_posts.non_translated + @tags = AdminPostTag.order(:name) + end + end + private def admin_post_params diff --git a/app/helpers/admin_post_helper.rb b/app/helpers/admin_post_helper.rb index e167de609f7..94e30b2c5d5 100644 --- a/app/helpers/admin_post_helper.rb +++ b/app/helpers/admin_post_helper.rb @@ -1,6 +1,9 @@ module AdminPostHelper def sorted_translations(admin_post) - admin_post.translations.sort_by do |translation| + translations = admin_post.translations + translations = translations.posted if admin_post.posted? + + translations.sort_by do |translation| language = translation.language language.sortable_name.blank? ? language.short : language.sortable_name end diff --git a/app/models/admin_post.rb b/app/models/admin_post.rb index 47ed4a87587..0af1629d0c2 100644 --- a/app/models/admin_post.rb +++ b/app/models/admin_post.rb @@ -36,16 +36,22 @@ class AdminPost < ApplicationRecord scope :non_translated, -> { where("translated_post_id IS NULL") } - scope :for_homepage, -> { order("created_at DESC").limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_ON_HOMEPAGE) } + scope :for_homepage, -> { posted.order(published_at: :desc).limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_ON_HOMEPAGE) } + + scope :unposted, -> { where(posted: false) } + scope :posted, -> { where(posted: true) } before_save :inherit_translated_post_comment_permissions, :inherit_translated_post_tags + before_save :set_published_at, if: :posted_changed? after_save :expire_cached_home_admin_posts, :update_translation_comment_permissions, :update_translation_tags + after_save :post_translations, if: :saved_change_to_posted? after_destroy :expire_cached_home_admin_posts # Return the name to link comments to for this object def commentable_name self.title end + def commentable_owners begin [Admin.find(self.admin_id)] @@ -54,6 +60,10 @@ def commentable_owners end end + def draft? + !self.posted? + end + def tag_list tags.map{ |t| t.name }.join(", ") end @@ -137,4 +147,18 @@ def update_translation_tags end end end + + def set_published_at + self.published_at = Time.current if self.posted && !self.published_at + end + + def post_translations + return if translations.blank? || !self.posted + + transaction do + translations.find_each do |post| + post.update(posted: true, published_at: self.published_at) + end + end + end end diff --git a/app/policies/admin_post_policy.rb b/app/policies/admin_post_policy.rb index 957e1c46e53..97f5a46ad88 100644 --- a/app/policies/admin_post_policy.rb +++ b/app/policies/admin_post_policy.rb @@ -1,13 +1,25 @@ class AdminPostPolicy < ApplicationPolicy POSTING_ROLES = %w[superadmin board board_assistants_team communications support translation].freeze + DRAFTING_ROLES = %w[policy_and_abuse].freeze def can_post? user_has_roles?(POSTING_ROLES) end - alias new? can_post? - alias edit? can_post? - alias create? can_post? - alias update? can_post? - alias destroy? can_post? + def can_draft? + user_has_roles?(DRAFTING_ROLES) || can_post? + end + + def edit? + can_post? || (@record&.draft? && can_draft?) + end + + alias new? can_draft? + alias show? can_draft? + alias create? edit? + alias update? edit? + alias destroy? edit? + alias post? can_post? + alias drafts? can_draft? + alias preview? edit? end diff --git a/app/views/admin/_admin_nav.html.erb b/app/views/admin/_admin_nav.html.erb index e705ea49bb8..59d920ce235 100644 --- a/app/views/admin/_admin_nav.html.erb +++ b/app/views/admin/_admin_nav.html.erb @@ -3,10 +3,17 @@
  • <%= span_if_current t(".ao3_news"), admin_posts_path %>
  • +
  • + <%= span_if_current t(".ao3_news_drafts"), drafts_admin_posts_path %> +
  • <% if policy(AdminPost).can_post? %>
  • <%= span_if_current t(".post_ao3_news"), new_admin_post_path %>
  • + <% elsif policy(AdminPost).can_draft? %> +
  • + <%= span_if_current t(".draft_ao3_news"), new_admin_post_path %> +
  • <% end %> <% if params[:controller] == "admin_posts" && params[:action] == "edit" %>
  • diff --git a/app/views/admin/_header.html.erb b/app/views/admin/_header.html.erb index 7fdbb92101b..8c4e3fc15d5 100644 --- a/app/views/admin/_header.html.erb +++ b/app/views/admin/_header.html.erb @@ -26,8 +26,13 @@ <%= link_to t(".nav.posts.admin_posts"), admin_posts_path %>