diff --git a/.rubocop.yml b/.rubocop.yml index 903f1c7b7..4b4dec97a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -939,9 +939,7 @@ RSpec/DescribedClassModuleWrapping: RSpec/ExampleLength: Description: Checks for long examples. - Enabled: true - Max: 15 - StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleLength + Enabled: false RSpec/IdenticalEqualityAssertion: Description: Checks for equality assertions with identical expressions on both sides. diff --git a/app/controllers/admin/display_ads_controller.rb b/app/controllers/admin/display_ads_controller.rb index 750f65c8b..cfd161c91 100644 --- a/app/controllers/admin/display_ads_controller.rb +++ b/app/controllers/admin/display_ads_controller.rb @@ -60,7 +60,7 @@ module Admin def display_ad_params params.permit(:organization_id, :body_markdown, :placement_area, :published, :approved, :name, :display_to, - :tag_list, :type_of) + :tag_list, :type_of, :exclude_article_ids) end def authorize_admin diff --git a/app/controllers/api/v1/display_ads_controller.rb b/app/controllers/api/v1/display_ads_controller.rb index 3dd3497c9..04e6dbaf2 100644 --- a/app/controllers/api/v1/display_ads_controller.rb +++ b/app/controllers/api/v1/display_ads_controller.rb @@ -52,7 +52,7 @@ module Api def permitted_params params.permit :approved, :body_markdown, :creator_id, :display_to, :name, :organization_id, :placement_area, :published, - :tag_list, :type_of + :tag_list, :type_of, :exclude_article_ids end end end diff --git a/app/javascript/packs/admin/displayAds.jsx b/app/javascript/packs/admin/displayAds.jsx index 6f807d32b..ec6577bed 100644 --- a/app/javascript/packs/admin/displayAds.jsx +++ b/app/javascript/packs/admin/displayAds.jsx @@ -74,6 +74,38 @@ function defaultTagValues() { return defaultValue; } +/** + * Shows and Renders Exclude Article IDs group + */ +function showExcludeIds() { + const excludeField = document.getElementsByClassName( + 'js-exclude-ids-textfield', + )[0].parentElement; + excludeField?.classList.remove('hidden'); +} + +/** + * Hides the Exclude Article IDs group + */ +function hideExcludeIds() { + const excludeField = document.getElementsByClassName( + 'js-exclude-ids-textfield', + )[0].parentElement; + excludeField?.classList.add('hidden'); +} + +/** + * Clears the content (i.e. value) of the Exclude Article IDs group + */ +function clearExcludeIds() { + const excludeField = document.getElementsByClassName( + 'js-exclude-ids-textfield', + )[0]; + if (excludeField) { + excludeField.value = ''; + } +} + /** * Shows and sets up the Targeted Tag(s) field if the placement area value is "post_comments". * Listens for change events on the select placement area dropdown @@ -81,17 +113,22 @@ function defaultTagValues() { */ document.ready.then(() => { const select = document.getElementsByClassName('js-placement-area')[0]; - const placementAreasWithTags = ['post_comments', 'post_sidebar'] - if (placementAreasWithTags.includes(select.value)) { + const articleSpecificPlacement = ['post_comments', 'post_sidebar']; + if (articleSpecificPlacement.includes(select.value)) { showTagsField(); + showExcludeIds(); } select.addEventListener('change', (event) => { - if (placementAreasWithTags.includes(event.target.value)) { + if (articleSpecificPlacement.includes(event.target.value)) { showTagsField(); + showExcludeIds(); } else { hideTagsField(); clearTagList(); + + hideExcludeIds(); + clearExcludeIds(); } }); }); diff --git a/app/models/display_ad.rb b/app/models/display_ad.rb index f1e59d140..2803d3ed8 100644 --- a/app/models/display_ad.rb +++ b/app/models/display_ad.rb @@ -37,12 +37,14 @@ class DisplayAd < ApplicationRecord search: "%#{term}%" } - def self.for_display(area:, user_signed_in:, organization_id: nil, article_tags: [], permit_adjacent_sponsors: true) + def self.for_display(area:, user_signed_in:, organization_id: nil, article_id: nil, + article_tags: [], permit_adjacent_sponsors: true) ads_for_display = DisplayAds::FilteredAdsQuery.call( display_ads: self, area: area, organization_id: organization_id, user_signed_in: user_signed_in, + article_id: article_id, article_tags: article_tags, permit_adjacent_sponsors: permit_adjacent_sponsors, ) @@ -80,10 +82,22 @@ class DisplayAd < ApplicationRecord # This needs to correspond with Rails built-in method signature # rubocop:disable Style/OptionHash def as_json(options = {}) - super(options.merge(except: %i[tags tag_list])).merge("tag_list" => cached_tag_list) + overrides = { + "tag_list" => cached_tag_list, + "exclude_article_ids" => exclude_article_ids.join(",") + } + super(options.merge(except: %i[tags tag_list])).merge(overrides) end # rubocop:enable Style/OptionHash + # exclude_article_ids is an integer array, web inputs are comma-separated strings + # ActiveRecord normalizes these in a bad way, so we are intervening + def exclude_article_ids=(input) + adjusted_input = input.is_a?(String) ? input.split(",") : input + adjusted_input = adjusted_input&.filter_map { |value| value.presence&.to_i } + write_attribute :exclude_article_ids, (adjusted_input || []) + end + private def generate_display_ad_name diff --git a/app/queries/display_ads/filtered_ads_query.rb b/app/queries/display_ads/filtered_ads_query.rb index fe125b7e0..7b91ebe77 100644 --- a/app/queries/display_ads/filtered_ads_query.rb +++ b/app/queries/display_ads/filtered_ads_query.rb @@ -5,12 +5,13 @@ module DisplayAds end def initialize(area:, user_signed_in:, organization_id: nil, article_tags: [], - permit_adjacent_sponsors: true, display_ads: DisplayAd) + permit_adjacent_sponsors: true, article_id: nil, display_ads: DisplayAd) @filtered_display_ads = display_ads.includes([:organization]) @area = area @user_signed_in = user_signed_in @organization_id = organization_id @article_tags = article_tags + @article_id = article_id @permit_adjacent_sponsors = permit_adjacent_sponsors end @@ -26,6 +27,10 @@ module DisplayAds @filtered_display_ads = untagged_post_comment_ads end + if @article_id.present? + @filtered_display_ads = unexcluded_article_ads + end + @filtered_display_ads = if @user_signed_in authenticated_ads(%w[all logged_in]) else @@ -59,6 +64,10 @@ module DisplayAds @filtered_display_ads.where(cached_tag_list: "") end + def unexcluded_article_ads + @filtered_display_ads.where("NOT (:id = ANY(exclude_article_ids))", id: @article_id) + end + def authenticated_ads(display_auth_audience) @filtered_display_ads.where(display_to: display_auth_audience) end diff --git a/app/views/admin/display_ads/_form.html.erb b/app/views/admin/display_ads/_form.html.erb index 9c04dc5b5..2e24c88d2 100644 --- a/app/views/admin/display_ads/_form.html.erb +++ b/app/views/admin/display_ads/_form.html.erb @@ -36,6 +36,11 @@ <%= text_field_tag :tag_list, @display_ad.tag_list.to_s, class: "crayons-textfield js-tags-textfield", autocomplete: "off" %> +
+