From 2afa5c18659931b1ee53b04450222f11f1f209db Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:13:25 +0000 Subject: [PATCH 1/9] AO3-6022 don't show hidden related works on related work pages --- app/controllers/related_works_controller.rb | 13 ++-- app/models/related_work.rb | 16 ++++- app/views/users/_sidebar.html.erb | 2 +- features/works/work_related.feature | 76 +++++++++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/app/controllers/related_works_controller.rb b/app/controllers/related_works_controller.rb index b740940734..20fabe0c14 100644 --- a/app/controllers/related_works_controller.rb +++ b/app/controllers/related_works_controller.rb @@ -10,15 +10,20 @@ def index @remixes_of_user = @user.related_works.posted.where(translation: false) @translations_by_user = @user.parent_work_relationships.posted.where(translation: true) @remixes_by_user = @user.parent_work_relationships.posted.where(translation: false) + return if logged_in_as_admin? + @translations_of_user = @translations_of_user.unhidden + @remixes_of_user = @remixes_of_user.unhidden + @translations_by_user = @translations_by_user.with_unhidden_parents + @remixes_by_user = @remixes_by_user.with_unhidden_parents return if @user == current_user # Extra constraints on what we display if someone else is viewing @user's # related works page: - @translations_of_user = @translations_of_user.merge(Work.revealed.non_anon).where(reciprocal: true) - @remixes_of_user = @remixes_of_user.merge(Work.revealed.non_anon).where(reciprocal: true) - @translations_by_user = @translations_by_user.merge(Work.revealed.non_anon).where(reciprocal: true) - @remixes_by_user = @remixes_by_user.merge(Work.revealed.non_anon).where(reciprocal: true) + @translations_of_user = @translations_of_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) + @remixes_of_user = @remixes_of_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) + @translations_by_user = @translations_by_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) + @remixes_by_user = @remixes_by_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) end # GET /related_works/1 diff --git a/app/models/related_work.rb b/app/models/related_work.rb index 97d7dc4a4d..82427c7bd0 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -7,9 +7,19 @@ class RelatedWork < ActiveRecord::Base attribute :author, :string attribute :language_id, :integer - scope :posted, -> { - joins("INNER JOIN `works` `child_works` ON `child_works`.`id` = `related_works`.`work_id`"). - where("child_works.posted = 1") + scope :posted, lambda { + joins("INNER JOIN `works` `child_works` ON `child_works`.`id` = `related_works`.`work_id`") + .where("child_works.posted = 1") + } + + scope :with_unhidden_parents, lambda { + joins("INNER JOIN `works` `parent_works` ON `parent_works`.`id` = `related_works`.`parent_id`") + .where("parent_works.hidden_by_admin = false") + } + + scope :unhidden, lambda { + joins("INNER JOIN `works` `child_works` ON `child_works`.`id` = `related_works`.`work_id`") + .where("child_works.hidden_by_admin = false") } before_validation :set_parent, if: :new_record? diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb index c5836c303a..2c92d0e01a 100644 --- a/app/views/users/_sidebar.html.erb +++ b/app/views/users/_sidebar.html.erb @@ -61,7 +61,7 @@
  • <%= span_if_current t(".switch.sign_ups", signup_number: @user.challenge_signups.count), user_signups_path(@user) %>
  • <%= span_if_current t(".switch.assignments", assignment_number: @user.assignments.unposted.undefaulted.count), user_assignments_path(@user) %>
  • <%= span_if_current t(".switch.claims", claim_number: @user.request_claims.unposted.count), user_claims_path(@user) %>
  • -
  • <%= span_if_current t(".switch.related_works", related_works_number: (@user.related_works.posted.count + @user.parent_work_relationships.count)), user_related_works_path(@user) %>
  • +
  • <%= span_if_current t(".switch.related_works", related_works_number: (@user.related_works.posted.unhidden.count + @user.parent_work_relationships.with_unhidden_parents.count)), user_related_works_path(@user) %>
  • <% end %>
  • <%= gifts_link(@user) %>
  • diff --git a/features/works/work_related.feature b/features/works/work_related.feature index b68699c495..c91eb5e16a 100644 --- a/features/works/work_related.feature +++ b/features/works/work_related.feature @@ -373,6 +373,82 @@ Scenario: Anonymous works listed as inspiration should have links to the authors Then I should see "Works inspired by this one: Followup by Anonymous" And I should not see "remixer" within ".afterword .children" +Scenario: Hidden inspired and inspiring works should not be listed on related work pages for users who can't access them + Given I have related works setup + And a related work has been posted and approved + When I am logged in as a "policy_and_abuse" admin + And I hide the work "Followup" + And I go to inspiration's related works page + Then I should see "Followup" + When I go to remixer's related works page + Then I should see "Followup" + When I am logged in as "remixer" + And I view my related works + Then I should see "Related Works (1)" + And I should see "Followup" + When I go to inspiration's related works page + Then I should not see "Followup" + When I am logged in as "inspiration" + And I view my related works + Then I should see "Related Works (0)" + And I should not see "Followup" + + When I am logged in as a "policy_and_abuse" admin + And I unhide the work "Followup" + And I hide the work "Worldbuilding" + And I go to inspiration's related works page + Then I should see "Worldbuilding" + When I go to remixer's related works page + Then I should see "Worldbuilding" + When I am logged in as "inspiration" + And I view my related works + Then I should see "Related Works (1)" + And I should see "Worldbuilding" + When I go to remixer's related works page + Then I should not see "Worldbuilding" + When I am logged in as "remixer" + And I view my related works + Then I should see "Related Works (0)" + And I should not see "Worldbuilding" + +Scenario: Hidden translations and translated works should not be listed on related work pages for users who can't access them + Given I have related works setup + And a translation has been posted and approved + When I am logged in as a "policy_and_abuse" admin + And I hide the work "Worldbuilding Translated" + And I go to inspiration's related works page + Then I should see "Worldbuilding Translated" + When I go to translator's related works page + Then I should see "Worldbuilding Translated" + When I am logged in as "translator" + And I view my related works + Then I should see "Related Works (1)" + And I should see "Worldbuilding Translated" + When I go to inspiration's related works page + Then I should not see "Worldbuilding Translated" + When I am logged in as "inspiration" + And I view my related works + Then I should see "Related Works (0)" + And I should not see "Worldbuilding Translated" + + When I am logged in as a "policy_and_abuse" admin + And I unhide the work "Worldbuilding Translated" + And I hide the work "Worldbuilding" + And I go to inspiration's related works page + Then I should see "Worldbuilding" + When I go to translator's related works page + Then I should see "Worldbuilding" + When I am logged in as "inspiration" + And I view my related works + Then I should see "Related Works (1)" + And I should see "Worldbuilding" + When I go to translator's related works page + Then I should not see "Worldbuilding" + When I am logged in as "translator" + And I view my related works + Then I should see "Related Works (0)" + And I should not see "Worldbuilding" + Scenario: When a user is notified that a co-authored work has been inspired by a work they posted, the e-mail should link to each author's URL instead of showing escaped HTML Given I have related works setup From 981235f31239123a81e4ee68e6828202e0097ba4 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:04:31 +0200 Subject: [PATCH 2/9] AO3-6022 a hacky way to make it also work in tests --- app/controllers/related_works_controller.rb | 23 +++++++++++++-------- app/models/related_work.rb | 13 +++++++++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/controllers/related_works_controller.rb b/app/controllers/related_works_controller.rb index 20fabe0c14..6d535ea492 100644 --- a/app/controllers/related_works_controller.rb +++ b/app/controllers/related_works_controller.rb @@ -12,18 +12,23 @@ def index @remixes_by_user = @user.parent_work_relationships.posted.where(translation: false) return if logged_in_as_admin? + translations_by_user_local = @translations_by_user.with_unhidden_parents + translations_by_user_external = @translations_by_user.with_unhidden_external_parents + remixes_by_user_local = @remixes_by_user.with_unhidden_parents + remixes_by_user_external = @remixes_by_user.with_unhidden_external_parents + @translations_of_user = @translations_of_user.unhidden @remixes_of_user = @remixes_of_user.unhidden - @translations_by_user = @translations_by_user.with_unhidden_parents - @remixes_by_user = @remixes_by_user.with_unhidden_parents - return if @user == current_user + if (@user == current_user) + @translations_by_user = (translations_by_user_local + translations_by_user_external).uniq + @remixes_by_user = (remixes_by_user_local + remixes_by_user_external).uniq + return + end - # Extra constraints on what we display if someone else is viewing @user's - # related works page: - @translations_of_user = @translations_of_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) - @remixes_of_user = @remixes_of_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) - @translations_by_user = @translations_by_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) - @remixes_by_user = @remixes_by_user.merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) + @translations_of_user = @translations_of_user.visible_to_all + @remixes_of_user = @remixes_of_user.visible_to_all + @translations_by_user = (translations_by_user_local.visible_to_all + translations_by_user_external.visible_to_all).uniq + @remixes_by_user = (remixes_by_user_local.visible_to_all + remixes_by_user_external.visible_to_all).uniq end # GET /related_works/1 diff --git a/app/models/related_work.rb b/app/models/related_work.rb index 82427c7bd0..b90d47b3d8 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -8,20 +8,27 @@ class RelatedWork < ActiveRecord::Base attribute :language_id, :integer scope :posted, lambda { - joins("INNER JOIN `works` `child_works` ON `child_works`.`id` = `related_works`.`work_id`") + joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") .where("child_works.posted = 1") } scope :with_unhidden_parents, lambda { - joins("INNER JOIN `works` `parent_works` ON `parent_works`.`id` = `related_works`.`parent_id`") + joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") + .where("parent_works.hidden_by_admin = false") + } + + scope :with_unhidden_external_parents, lambda { + joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") .where("parent_works.hidden_by_admin = false") } scope :unhidden, lambda { - joins("INNER JOIN `works` `child_works` ON `child_works`.`id` = `related_works`.`work_id`") + joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") .where("child_works.hidden_by_admin = false") } + scope :visible_to_all, -> { merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) } + before_validation :set_parent, if: :new_record? def set_parent return if parent From e579e38a2256a270b70832e7482962d286f6f1dd Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:39:50 +0200 Subject: [PATCH 3/9] AO3-6022 refactored + sidebar counts should also be correct in tests --- app/controllers/related_works_controller.rb | 26 +++--------- app/models/related_work.rb | 46 ++++++++++++++++----- app/views/users/_sidebar.html.erb | 2 +- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/app/controllers/related_works_controller.rb b/app/controllers/related_works_controller.rb index 6d535ea492..1d34715da1 100644 --- a/app/controllers/related_works_controller.rb +++ b/app/controllers/related_works_controller.rb @@ -6,29 +6,15 @@ class RelatedWorksController < ApplicationController def index @page_subtitle = t(".page_title", login: @user.login) - @translations_of_user = @user.related_works.posted.where(translation: true) - @remixes_of_user = @user.related_works.posted.where(translation: false) - @translations_by_user = @user.parent_work_relationships.posted.where(translation: true) - @remixes_by_user = @user.parent_work_relationships.posted.where(translation: false) - return if logged_in_as_admin? - translations_by_user_local = @translations_by_user.with_unhidden_parents - translations_by_user_external = @translations_by_user.with_unhidden_external_parents - remixes_by_user_local = @remixes_by_user.with_unhidden_parents - remixes_by_user_external = @remixes_by_user.with_unhidden_external_parents + @translations_of_user = @user.related_works.translations.visible_on_user_page(@user).visible + @remixes_of_user = @user.related_works.remixes.visible_on_user_page(@user).visible - @translations_of_user = @translations_of_user.unhidden - @remixes_of_user = @remixes_of_user.unhidden - if (@user == current_user) - @translations_by_user = (translations_by_user_local + translations_by_user_external).uniq - @remixes_by_user = (remixes_by_user_local + remixes_by_user_external).uniq - return - end + translations_by_user = @user.parent_work_relationships.translations.visible_on_user_page(@user) + remixes_by_user = @user.parent_work_relationships.remixes.visible_on_user_page(@user) - @translations_of_user = @translations_of_user.visible_to_all - @remixes_of_user = @remixes_of_user.visible_to_all - @translations_by_user = (translations_by_user_local.visible_to_all + translations_by_user_external.visible_to_all).uniq - @remixes_by_user = (remixes_by_user_local.visible_to_all + remixes_by_user_external.visible_to_all).uniq + @translations_by_user = (translations_by_user.of_visible_local_works + translations_by_user.of_visible_external_works).sort + @remixes_by_user = (remixes_by_user.of_visible_local_works + remixes_by_user.of_visible_external_works).sort end # GET /related_works/1 diff --git a/app/models/related_work.rb b/app/models/related_work.rb index b90d47b3d8..7ed2952c93 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -7,27 +7,53 @@ class RelatedWork < ActiveRecord::Base attribute :author, :string attribute :language_id, :integer + scope :translations, -> { where(translation: true) } + scope :remixes, -> { where(translation: false) } + scope :posted, lambda { joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") .where("child_works.posted = 1") } - scope :with_unhidden_parents, lambda { - joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") - .where("parent_works.hidden_by_admin = false") - } - - scope :with_unhidden_external_parents, lambda { - joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") - .where("parent_works.hidden_by_admin = false") - } + scope :reciprocal, -> { where(reciprocal: true) } scope :unhidden, lambda { joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") .where("child_works.hidden_by_admin = false") } - scope :visible_to_all, -> { merge(Work.revealed.non_anon.unhidden).where(reciprocal: true) } + def self.visible_on_user_page(user) + if User.current_user.is_a?(Admin) || user == User.current_user + posted + else + posted.reciprocal.merge(Work.revealed.non_anon.unhidden) + end + end + + scope :visible, -> { unhidden unless User.current_user.is_a?(Admin) } + + scope :of_local_works, -> { where(parent_type: Work) } + scope :of_external_works, -> { where(parent_type: ExternalWork) } + + scope :of_visible_local_works, lambda { + if User.current_user.is_a? Admin + of_local_works + else + of_local_works + .joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") + .where("parent_works.hidden_by_admin = false") + end + } + + scope :of_visible_external_works, lambda { + if User.current_user.is_a? Admin + of_external_works + else + of_external_works + .joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") + .where("parent_works.hidden_by_admin = false") + end + } before_validation :set_parent, if: :new_record? def set_parent diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb index 2c92d0e01a..c06ae80515 100644 --- a/app/views/users/_sidebar.html.erb +++ b/app/views/users/_sidebar.html.erb @@ -61,7 +61,7 @@
  • <%= span_if_current t(".switch.sign_ups", signup_number: @user.challenge_signups.count), user_signups_path(@user) %>
  • <%= span_if_current t(".switch.assignments", assignment_number: @user.assignments.unposted.undefaulted.count), user_assignments_path(@user) %>
  • <%= span_if_current t(".switch.claims", claim_number: @user.request_claims.unposted.count), user_claims_path(@user) %>
  • -
  • <%= span_if_current t(".switch.related_works", related_works_number: (@user.related_works.posted.unhidden.count + @user.parent_work_relationships.with_unhidden_parents.count)), user_related_works_path(@user) %>
  • +
  • <%= span_if_current t(".switch.related_works", related_works_number: (@user.related_works.visible_on_user_page(@user).visible.count + @user.parent_work_relationships.visible_on_user_page(@user).of_visible_local_works.count + @user.parent_work_relationships.visible_on_user_page(@user).of_visible_external_works.count)), user_related_works_path(@user) %>
  • <% end %>
  • <%= gifts_link(@user) %>
  • From fb05ddf0abcef1de6becc20724bc3c3821ae9fc4 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Tue, 12 May 2026 23:38:38 +0200 Subject: [PATCH 4/9] AO3-6022 hide hidden works from everyone --- app/controllers/related_works_controller.rb | 15 +++++---- app/helpers/related_works_helper.rb | 10 ++++++ app/models/related_work.rb | 24 +++++--------- app/views/users/_sidebar.html.erb | 2 +- features/works/work_related.feature | 36 ++++++++++----------- 5 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 app/helpers/related_works_helper.rb diff --git a/app/controllers/related_works_controller.rb b/app/controllers/related_works_controller.rb index 1d34715da1..d189c8544b 100644 --- a/app/controllers/related_works_controller.rb +++ b/app/controllers/related_works_controller.rb @@ -7,14 +7,15 @@ class RelatedWorksController < ApplicationController def index @page_subtitle = t(".page_title", login: @user.login) - @translations_of_user = @user.related_works.translations.visible_on_user_page(@user).visible - @remixes_of_user = @user.related_works.remixes.visible_on_user_page(@user).visible + related_works = @user.related_works.visible_on_user_page(@user).visible_works + parent_work_relationships = @user.parent_work_relationships.visible_on_user_page(@user) + local_parent_work_relationships = parent_work_relationships.of_visible_local_works + external_parent_work_relationships = parent_work_relationships.of_visible_external_works - translations_by_user = @user.parent_work_relationships.translations.visible_on_user_page(@user) - remixes_by_user = @user.parent_work_relationships.remixes.visible_on_user_page(@user) - - @translations_by_user = (translations_by_user.of_visible_local_works + translations_by_user.of_visible_external_works).sort - @remixes_by_user = (remixes_by_user.of_visible_local_works + remixes_by_user.of_visible_external_works).sort + @translations_of_user = related_works.translations + @remixes_of_user = related_works.remixes + @translations_by_user = (local_parent_work_relationships.translations + external_parent_work_relationships.translations).sort + @remixes_by_user = (local_parent_work_relationships.remixes + external_parent_work_relationships.remixes).sort end # GET /related_works/1 diff --git a/app/helpers/related_works_helper.rb b/app/helpers/related_works_helper.rb new file mode 100644 index 0000000000..c289ec8f95 --- /dev/null +++ b/app/helpers/related_works_helper.rb @@ -0,0 +1,10 @@ +module RelatedWorksHelper + def related_works_count(user) + related_works = user.related_works.visible_on_user_page(user).visible_works + parent_work_relationships = user.parent_work_relationships.visible_on_user_page(user) + local_parent_work_relationships = parent_work_relationships.of_visible_local_works + external_parent_work_relationships = parent_work_relationships.of_visible_external_works + + return related_works.count + local_parent_work_relationships.count + external_parent_work_relationships.count + end +end diff --git a/app/models/related_work.rb b/app/models/related_work.rb index 7ed2952c93..ed4be88e15 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -24,35 +24,27 @@ class RelatedWork < ActiveRecord::Base def self.visible_on_user_page(user) if User.current_user.is_a?(Admin) || user == User.current_user - posted + posted.merge(Work.unhidden) else posted.reciprocal.merge(Work.revealed.non_anon.unhidden) end end - scope :visible, -> { unhidden unless User.current_user.is_a?(Admin) } + scope :visible_works, -> { unhidden } scope :of_local_works, -> { where(parent_type: Work) } scope :of_external_works, -> { where(parent_type: ExternalWork) } scope :of_visible_local_works, lambda { - if User.current_user.is_a? Admin - of_local_works - else - of_local_works - .joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") - .where("parent_works.hidden_by_admin = false") - end + of_local_works + .joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") + .where("parent_works.hidden_by_admin = false") } scope :of_visible_external_works, lambda { - if User.current_user.is_a? Admin - of_external_works - else - of_external_works - .joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") - .where("parent_works.hidden_by_admin = false") - end + of_external_works + .joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") + .where("parent_works.hidden_by_admin = false") } before_validation :set_parent, if: :new_record? diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb index c06ae80515..7b23c7b158 100644 --- a/app/views/users/_sidebar.html.erb +++ b/app/views/users/_sidebar.html.erb @@ -61,7 +61,7 @@
  • <%= span_if_current t(".switch.sign_ups", signup_number: @user.challenge_signups.count), user_signups_path(@user) %>
  • <%= span_if_current t(".switch.assignments", assignment_number: @user.assignments.unposted.undefaulted.count), user_assignments_path(@user) %>
  • <%= span_if_current t(".switch.claims", claim_number: @user.request_claims.unposted.count), user_claims_path(@user) %>
  • -
  • <%= span_if_current t(".switch.related_works", related_works_number: (@user.related_works.visible_on_user_page(@user).visible.count + @user.parent_work_relationships.visible_on_user_page(@user).of_visible_local_works.count + @user.parent_work_relationships.visible_on_user_page(@user).of_visible_external_works.count)), user_related_works_path(@user) %>
  • +
  • <%= span_if_current t(".switch.related_works", related_works_number: related_works_count(@user)), user_related_works_path(@user) %>
  • <% end %>
  • <%= gifts_link(@user) %>
  • diff --git a/features/works/work_related.feature b/features/works/work_related.feature index c91eb5e16a..7ab816f4ec 100644 --- a/features/works/work_related.feature +++ b/features/works/work_related.feature @@ -373,19 +373,19 @@ Scenario: Anonymous works listed as inspiration should have links to the authors Then I should see "Works inspired by this one: Followup by Anonymous" And I should not see "remixer" within ".afterword .children" -Scenario: Hidden inspired and inspiring works should not be listed on related work pages for users who can't access them +Scenario: Hidden inspired and inspiring works should not be listed on related work pages Given I have related works setup And a related work has been posted and approved When I am logged in as a "policy_and_abuse" admin And I hide the work "Followup" And I go to inspiration's related works page - Then I should see "Followup" + Then I should not see "Followup" When I go to remixer's related works page - Then I should see "Followup" + Then I should not see "Followup" When I am logged in as "remixer" And I view my related works - Then I should see "Related Works (1)" - And I should see "Followup" + Then I should see "Related Works (0)" + And I should not see "Followup" When I go to inspiration's related works page Then I should not see "Followup" When I am logged in as "inspiration" @@ -397,13 +397,13 @@ Scenario: Hidden inspired and inspiring works should not be listed on related wo And I unhide the work "Followup" And I hide the work "Worldbuilding" And I go to inspiration's related works page - Then I should see "Worldbuilding" + Then I should not see "Worldbuilding" When I go to remixer's related works page - Then I should see "Worldbuilding" + Then I should not see "Worldbuilding" When I am logged in as "inspiration" And I view my related works - Then I should see "Related Works (1)" - And I should see "Worldbuilding" + Then I should see "Related Works (0)" + And I should not see "Worldbuilding" When I go to remixer's related works page Then I should not see "Worldbuilding" When I am logged in as "remixer" @@ -411,19 +411,19 @@ Scenario: Hidden inspired and inspiring works should not be listed on related wo Then I should see "Related Works (0)" And I should not see "Worldbuilding" -Scenario: Hidden translations and translated works should not be listed on related work pages for users who can't access them +Scenario: Hidden translations and translated works should not be listed on related work pages Given I have related works setup And a translation has been posted and approved When I am logged in as a "policy_and_abuse" admin And I hide the work "Worldbuilding Translated" And I go to inspiration's related works page - Then I should see "Worldbuilding Translated" + Then I should not see "Worldbuilding Translated" When I go to translator's related works page - Then I should see "Worldbuilding Translated" + Then I should not see "Worldbuilding Translated" When I am logged in as "translator" And I view my related works - Then I should see "Related Works (1)" - And I should see "Worldbuilding Translated" + Then I should see "Related Works (0)" + And I should not see "Worldbuilding Translated" When I go to inspiration's related works page Then I should not see "Worldbuilding Translated" When I am logged in as "inspiration" @@ -435,13 +435,13 @@ Scenario: Hidden translations and translated works should not be listed on relat And I unhide the work "Worldbuilding Translated" And I hide the work "Worldbuilding" And I go to inspiration's related works page - Then I should see "Worldbuilding" + Then I should not see "Worldbuilding" When I go to translator's related works page - Then I should see "Worldbuilding" + Then I should not see "Worldbuilding" When I am logged in as "inspiration" And I view my related works - Then I should see "Related Works (1)" - And I should see "Worldbuilding" + Then I should see "Related Works (0)" + And I should not see "Worldbuilding" When I go to translator's related works page Then I should not see "Worldbuilding" When I am logged in as "translator" From 28cddbbb674cec2e5b736e591f7ba9f4fe944992 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Wed, 13 May 2026 17:06:16 +0200 Subject: [PATCH 5/9] AO3-6022 add comment + slightly rearrange the scopes --- app/models/related_work.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/models/related_work.rb b/app/models/related_work.rb index ed4be88e15..0b2a2e1b84 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -9,19 +9,13 @@ class RelatedWork < ActiveRecord::Base scope :translations, -> { where(translation: true) } scope :remixes, -> { where(translation: false) } + scope :reciprocal, -> { where(reciprocal: true) } scope :posted, lambda { joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") .where("child_works.posted = 1") } - scope :reciprocal, -> { where(reciprocal: true) } - - scope :unhidden, lambda { - joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") - .where("child_works.hidden_by_admin = false") - } - def self.visible_on_user_page(user) if User.current_user.is_a?(Admin) || user == User.current_user posted.merge(Work.unhidden) @@ -30,8 +24,17 @@ def self.visible_on_user_page(user) end end + scope :unhidden, lambda { + joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") + .where("child_works.hidden_by_admin = false") + } + scope :visible_works, -> { unhidden } + # Separate scopes for local and external works to make tests work + # (the join in of_visible_local_works gets both local and external works + # in web environments, but only local ones in automated tests) + scope :of_local_works, -> { where(parent_type: Work) } scope :of_external_works, -> { where(parent_type: ExternalWork) } From 047be41f3872dcacd0d7562fb4641b021f610a08 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Wed, 13 May 2026 19:25:46 +0000 Subject: [PATCH 6/9] AO3-5550 hide relations that include restricted works from guests --- app/models/related_work.rb | 31 +++++++++++++++---- features/works/work_related.feature | 46 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/app/models/related_work.rb b/app/models/related_work.rb index 0b2a2e1b84..c7778f158d 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -19,8 +19,10 @@ class RelatedWork < ActiveRecord::Base def self.visible_on_user_page(user) if User.current_user.is_a?(Admin) || user == User.current_user posted.merge(Work.unhidden) - else + elsif User.current_user.is_a?(User) posted.reciprocal.merge(Work.revealed.non_anon.unhidden) + else + posted.reciprocal.merge(Work.revealed.non_anon.unhidden.unrestricted) end end @@ -29,21 +31,40 @@ def self.visible_on_user_page(user) .where("child_works.hidden_by_admin = false") } - scope :visible_works, -> { unhidden } + scope :unrestricted, lambda { + joins("INNER JOIN works child_works ON child_works.id = related_works.work_id") + .where("child_works.restricted = false") + } - # Separate scopes for local and external works to make tests work - # (the join in of_visible_local_works gets both local and external works + scope :visible_works, -> { + if User.current_user.present? + unhidden + else + unhidden.unrestricted + end + } + + # Separate scopes for local and external parent works to make tests work + # (the join in of_unhidden_local_works gets both local and external works # in web environments, but only local ones in automated tests) scope :of_local_works, -> { where(parent_type: Work) } scope :of_external_works, -> { where(parent_type: ExternalWork) } - scope :of_visible_local_works, lambda { + scope :of_unhidden_local_works, lambda { of_local_works .joins("INNER JOIN works parent_works ON parent_works.id = related_works.parent_id") .where("parent_works.hidden_by_admin = false") } + scope :of_visible_local_works, lambda { + if User.current_user.present? + of_unhidden_local_works + else + of_unhidden_local_works.where("parent_works.restricted = false") + end + } + scope :of_visible_external_works, lambda { of_external_works .joins("INNER JOIN external_works parent_works ON parent_works.id = related_works.parent_id") diff --git a/features/works/work_related.feature b/features/works/work_related.feature index 7ab816f4ec..7457e25e39 100644 --- a/features/works/work_related.feature +++ b/features/works/work_related.feature @@ -349,6 +349,52 @@ Scenario: Restricted works listed as Inspiration show up [Restricted] for guests And I view the work "Followup" Then I should see "Inspired by [Restricted Work] by inspiration" +Scenario: Restricted inspired and inspiring works should not be listed on related work pages for guests + Given I have related works setup + And a related work has been posted and approved + When I am logged in as "remixer" + And I lock the work "Followup" + When I am logged out + And I go to inspiration's related works page + Then I should not see "Followup" + And I should not see "Worldbuilding" + When I go to remixer's related works page + Then I should not see "Followup" + And I should not see "Worldbuilding" + + When I am logged in as "remixer" + And I unlock the work "Followup" + When I am logged in as "inspiration" + And I lock the work "Worldbuilding" + When I am logged out + And I go to inspiration's related works page + Then I should not see "Followup" + And I should not see "Worldbuilding" + When I go to remixer's related works page + Then I should not see "Followup" + And I should not see "Worldbuilding" + +Scenario: Restricted inspired and inspiring works should not be listed on related work pages for guests + Given I have related works setup + And a translation has been posted and approved + When I am logged in as "translator" + And I lock the work "Worldbuilding Translated" + When I am logged out + And I go to inspiration's related works page + Then I should not see "Worldbuilding" + When I go to translator's related works page + Then I should not see "Worldbuilding" + + When I am logged in as "translator" + And I unlock the work "Worldbuilding Translated" + When I am logged in as "inspiration" + And I lock the work "Worldbuilding" + When I am logged out + And I go to inspiration's related works page + Then I should not see "Worldbuilding" + When I go to translator's related works page + Then I should not see "Worldbuilding" + Scenario: Anonymous works listed as inspiration should have links to the authors, but only for the authors themselves and admins Given I have related works setup From 60aa30253f6e6a4e11218ee8bf8e07473559aaf1 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Fri, 15 May 2026 02:20:11 +0000 Subject: [PATCH 7/9] AO3-6022 rubocop --- app/helpers/related_works_helper.rb | 2 +- app/models/related_work.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/helpers/related_works_helper.rb b/app/helpers/related_works_helper.rb index c289ec8f95..d08df01c19 100644 --- a/app/helpers/related_works_helper.rb +++ b/app/helpers/related_works_helper.rb @@ -5,6 +5,6 @@ def related_works_count(user) local_parent_work_relationships = parent_work_relationships.of_visible_local_works external_parent_work_relationships = parent_work_relationships.of_visible_external_works - return related_works.count + local_parent_work_relationships.count + external_parent_work_relationships.count + related_works.count + local_parent_work_relationships.count + external_parent_work_relationships.count end end diff --git a/app/models/related_work.rb b/app/models/related_work.rb index c7778f158d..064497572e 100644 --- a/app/models/related_work.rb +++ b/app/models/related_work.rb @@ -17,12 +17,13 @@ class RelatedWork < ActiveRecord::Base } def self.visible_on_user_page(user) - if User.current_user.is_a?(Admin) || user == User.current_user + case User.current_user + when user || is_a?(Admin) posted.merge(Work.unhidden) - elsif User.current_user.is_a?(User) - posted.reciprocal.merge(Work.revealed.non_anon.unhidden) + when is_a?(User) + posted.reciprocal.merge(Work.unhidden.revealed.non_anon) else - posted.reciprocal.merge(Work.revealed.non_anon.unhidden.unrestricted) + posted.reciprocal.merge(Work.unhidden.revealed.non_anon.unrestricted) end end @@ -36,7 +37,7 @@ def self.visible_on_user_page(user) .where("child_works.restricted = false") } - scope :visible_works, -> { + scope :visible_works, lambda { if User.current_user.present? unhidden else From b26e0bed7ff8f50436e19606cbeb2f204cb064f7 Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Sat, 16 May 2026 04:50:28 +0200 Subject: [PATCH 8/9] AO3-6022 test improvements --- features/works/work_related.feature | 30 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/features/works/work_related.feature b/features/works/work_related.feature index 7457e25e39..78d905f1d0 100644 --- a/features/works/work_related.feature +++ b/features/works/work_related.feature @@ -335,38 +335,39 @@ Scenario: Restricted works listed as Inspiration show up [Restricted] for guests And a related work has been posted and approved When I am logged in as "remixer" And I lock the work "Followup" - When I am logged out + When I log out And I view the work "Worldbuilding" Then I should see "[Restricted Work] by remixer" When I am logged in as "remixer" And I unlock the work "Followup" - When I am logged out + When I log out And I view the work "Followup" Then I should see "Inspired by Worldbuilding by inspiration" When I am logged in as "inspiration" And I lock the work "Worldbuilding" - When I am logged out + When I log out And I view the work "Followup" Then I should see "Inspired by [Restricted Work] by inspiration" Scenario: Restricted inspired and inspiring works should not be listed on related work pages for guests Given I have related works setup And a related work has been posted and approved + # Restricted inspired work When I am logged in as "remixer" And I lock the work "Followup" - When I am logged out + When I log out And I go to inspiration's related works page Then I should not see "Followup" And I should not see "Worldbuilding" When I go to remixer's related works page Then I should not see "Followup" And I should not see "Worldbuilding" - + # Restricted inspiration When I am logged in as "remixer" And I unlock the work "Followup" When I am logged in as "inspiration" And I lock the work "Worldbuilding" - When I am logged out + When I log out And I go to inspiration's related works page Then I should not see "Followup" And I should not see "Worldbuilding" @@ -374,22 +375,23 @@ Scenario: Restricted inspired and inspiring works should not be listed on relate Then I should not see "Followup" And I should not see "Worldbuilding" -Scenario: Restricted inspired and inspiring works should not be listed on related work pages for guests +Scenario: Restricted translations and translated works should not be listed on related work pages for guests Given I have related works setup And a translation has been posted and approved + # Restricted translation When I am logged in as "translator" And I lock the work "Worldbuilding Translated" - When I am logged out + When I log out And I go to inspiration's related works page Then I should not see "Worldbuilding" When I go to translator's related works page Then I should not see "Worldbuilding" - + # Restricted translated work When I am logged in as "translator" And I unlock the work "Worldbuilding Translated" When I am logged in as "inspiration" And I lock the work "Worldbuilding" - When I am logged out + When I log out And I go to inspiration's related works page Then I should not see "Worldbuilding" When I go to translator's related works page @@ -414,7 +416,7 @@ Scenario: Anonymous works listed as inspiration should have links to the authors When I follow "remixer" within ".afterword .children" Then I should be on the dashboard page for user "remixer" with pseud "remixer" - When I am logged out + When I log out And I view the work "Worldbuilding" Then I should see "Works inspired by this one: Followup by Anonymous" And I should not see "remixer" within ".afterword .children" @@ -422,6 +424,7 @@ Scenario: Anonymous works listed as inspiration should have links to the authors Scenario: Hidden inspired and inspiring works should not be listed on related work pages Given I have related works setup And a related work has been posted and approved + # Hidden inspired work When I am logged in as a "policy_and_abuse" admin And I hide the work "Followup" And I go to inspiration's related works page @@ -438,7 +441,7 @@ Scenario: Hidden inspired and inspiring works should not be listed on related wo And I view my related works Then I should see "Related Works (0)" And I should not see "Followup" - + # Hidden inspiration When I am logged in as a "policy_and_abuse" admin And I unhide the work "Followup" And I hide the work "Worldbuilding" @@ -460,6 +463,7 @@ Scenario: Hidden inspired and inspiring works should not be listed on related wo Scenario: Hidden translations and translated works should not be listed on related work pages Given I have related works setup And a translation has been posted and approved + # Hidden translation When I am logged in as a "policy_and_abuse" admin And I hide the work "Worldbuilding Translated" And I go to inspiration's related works page @@ -476,7 +480,7 @@ Scenario: Hidden translations and translated works should not be listed on relat And I view my related works Then I should see "Related Works (0)" And I should not see "Worldbuilding Translated" - + # Hidden translated work When I am logged in as a "policy_and_abuse" admin And I unhide the work "Worldbuilding Translated" And I hide the work "Worldbuilding" From 20201876bc09d2474a02b0db24c7046c2c4f87eb Mon Sep 17 00:00:00 2001 From: Slava <53832230+slavalamp@users.noreply.github.com> Date: Sat, 16 May 2026 04:54:44 +0200 Subject: [PATCH 9/9] AO3-6022 ...undo the changes that i'm already doing in the other pr anyway --- features/works/work_related.feature | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/works/work_related.feature b/features/works/work_related.feature index 78d905f1d0..8e783abd52 100644 --- a/features/works/work_related.feature +++ b/features/works/work_related.feature @@ -335,17 +335,17 @@ Scenario: Restricted works listed as Inspiration show up [Restricted] for guests And a related work has been posted and approved When I am logged in as "remixer" And I lock the work "Followup" - When I log out + When I am logged out And I view the work "Worldbuilding" Then I should see "[Restricted Work] by remixer" When I am logged in as "remixer" And I unlock the work "Followup" - When I log out + When I am logged out And I view the work "Followup" Then I should see "Inspired by Worldbuilding by inspiration" When I am logged in as "inspiration" And I lock the work "Worldbuilding" - When I log out + When I am logged out And I view the work "Followup" Then I should see "Inspired by [Restricted Work] by inspiration" @@ -416,7 +416,7 @@ Scenario: Anonymous works listed as inspiration should have links to the authors When I follow "remixer" within ".afterword .children" Then I should be on the dashboard page for user "remixer" with pseud "remixer" - When I log out + When I am logged out And I view the work "Worldbuilding" Then I should see "Works inspired by this one: Followup by Anonymous" And I should not see "remixer" within ".afterword .children"