From 75d3a79c12b86d5a529095919740c4fe79d74a2d Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Mon, 25 Jun 2018 11:58:07 -0400 Subject: [PATCH] Add search_keywords record and associated functionality (#494) * Add search_keywords record and associated functionality * Fix test and add badge rewarder method --- app/labor/badge_rewarder.rb | 10 +++ app/models/article.rb | 15 ++++ app/models/search_keyword.rb | 17 +++++ .../articles/_sidebar_additional.html.erb | 68 +++++++++++++++---- .../20180624230435_create_search_keywords.rb | 14 ++++ db/schema.rb | 14 +++- spec/factories/search_keywords.rb | 10 +++ spec/labor/badge_rewarder_spec.rb | 9 +++ spec/models/article_spec.rb | 56 +++++++++++++++ spec/models/search_keyword_spec.rb | 22 ++++++ 10 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 app/models/search_keyword.rb create mode 100644 db/migrate/20180624230435_create_search_keywords.rb create mode 100644 spec/factories/search_keywords.rb create mode 100644 spec/models/search_keyword_spec.rb diff --git a/app/labor/badge_rewarder.rb b/app/labor/badge_rewarder.rb index 05bb5e321..cf8d286c6 100644 --- a/app/labor/badge_rewarder.rb +++ b/app/labor/badge_rewarder.rb @@ -22,4 +22,14 @@ class BadgeRewarder # ID 3 is the proper ID in prod. We should change in future to ENV var. end end + + def reward_top_seven_badges(usernames) + User.where(username: usernames).each do |user| + BadgeAchievement.create( + user_id: user.id, + badge_id: Badge.find_by_slug("top-7").id, + rewarding_context_message_markdown: "Congrats!!!") + user.save + end + end end diff --git a/app/models/article.rb b/app/models/article.rb index 3e98cf042..21d7b0b02 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -340,6 +340,21 @@ class Article < ApplicationRecord end end + def self.seo_boostable(tag=nil) + keyword_paths = SearchKeyword. + where("google_position > ? AND google_position < ? AND google_volume > ? AND google_difficulty < ?", + 3 , 20, 1000, 40).pluck(:google_result_path) + if tag + Article.where(path: keyword_paths, published: true, featured: true). + tagged_with(tag). + pluck(:path, :title, :comments_count, :created_at) + + else + Article.where(path: keyword_paths, published: true, featured: true). + pluck(:path, :title, :comments_count, :created_at) + end + end + def async_score_calc update_column(:hotness_score, BlackBox.article_hotness_score(self)) update_column(:spaminess_rating, BlackBox.calculate_spaminess(self)) diff --git a/app/models/search_keyword.rb b/app/models/search_keyword.rb new file mode 100644 index 000000000..bec230a2f --- /dev/null +++ b/app/models/search_keyword.rb @@ -0,0 +1,17 @@ +class SearchKeyword < ApplicationRecord + validates :keyword, presence: true + validates :google_result_path, presence: true + validates :google_position, presence: true + validates :google_volume, presence: true + validates :google_difficulty, presence: true + validates :google_checked_at, presence: true + validate :path_format + + private + + def path_format + unless google_result_path&.starts_with?("/") && google_result_path&.count("/") == 2 + errors.add(:google_result_path, "must start with / and be properly formatted") + end + end +end diff --git a/app/views/articles/_sidebar_additional.html.erb b/app/views/articles/_sidebar_additional.html.erb index c76bf769a..3ca64911e 100644 --- a/app/views/articles/_sidebar_additional.html.erb +++ b/app/views/articles/_sidebar_additional.html.erb @@ -45,6 +45,25 @@ <%= javascript_pack_tag "sidebarWidget", defer: true %>
">
+ <% else %> + <% cache("seo-boostable-posts-for-tag#{@tag}", :expires_in => 18.hours) do %> + <% @boostable_posts = Article.seo_boostable(@tag) %> + <% if @boostable_posts.any? %> +
+
+ trending guides/resources +
+
+ +
+
+ <% end %> + <% end %> <% end %> <% elsif @query %> <% elsif @podcast_index %> @@ -185,22 +204,43 @@ ASK FOR AN EXPLANATION -
-
- latest podcasts -
-
- <% @podcast_episodes.each do |ep| %> -
- <%= ep.title %> -
- <%= ep.podcast.title %> + <% if user_signed_in? %> + + <% else %> + <% cache("seo-boostable-posts-homepage", :expires_in => 18.hours) do %> + <% @boostable_posts = Article.seo_boostable %> + <% if @boostable_posts.any? %> +
+
+ trending guides/resources +
+
+
- <% end %> - VIEW ALL -
-
+
+ <% end %> + <% end %> + <% end %> <% end %>
diff --git a/db/migrate/20180624230435_create_search_keywords.rb b/db/migrate/20180624230435_create_search_keywords.rb new file mode 100644 index 000000000..2ee909d97 --- /dev/null +++ b/db/migrate/20180624230435_create_search_keywords.rb @@ -0,0 +1,14 @@ +class CreateSearchKeywords < ActiveRecord::Migration[5.1] + def change + create_table :search_keywords do |t| + t.string :keyword + t.string :google_result_path + t.integer :google_position + t.integer :google_volume + t.integer :google_difficulty + t.datetime :google_checked_at + t.timestamps + end + add_index :search_keywords, :google_result_path + end +end diff --git a/db/schema.rb b/db/schema.rb index 75df2aa1e..fa088fe27 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180622173538) do +ActiveRecord::Schema.define(version: 20180624230435) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -518,6 +518,18 @@ ActiveRecord::Schema.define(version: 20180622173538) do t.index ["name"], name: "index_roles_on_name" end + create_table "search_keywords", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "google_checked_at" + t.integer "google_difficulty" + t.integer "google_position" + t.string "google_result_path" + t.integer "google_volume" + t.string "keyword" + t.datetime "updated_at", null: false + t.index ["google_result_path"], name: "index_search_keywords_on_google_result_path" + end + create_table "taggings", id: :serial, force: :cascade do |t| t.string "context", limit: 128 t.datetime "created_at" diff --git a/spec/factories/search_keywords.rb b/spec/factories/search_keywords.rb new file mode 100644 index 000000000..4b86a90de --- /dev/null +++ b/spec/factories/search_keywords.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :search_keyword do + keyword { Faker::Book.title } + google_result_path { "/username_#{rand(10000)}/slug_#{rand(100000)}" } + google_position { rand(200) } + google_volume { rand(100000) } + google_difficulty { rand(100) } + google_checked_at { 3.weeks.ago } + end +end diff --git a/spec/labor/badge_rewarder_spec.rb b/spec/labor/badge_rewarder_spec.rb index c4859dbcf..621682b94 100644 --- a/spec/labor/badge_rewarder_spec.rb +++ b/spec/labor/badge_rewarder_spec.rb @@ -27,4 +27,13 @@ RSpec.describe BadgeRewarder do expect(user.badge_achievements.size).to eq(1) expect(user_other.badge_achievements.size).to eq(0) end + + it "rewards top seven badge to users" do + badge = create(:badge, title: "Top 7") + user = create(:user) + user_other = create(:user) + third_other = create(:user) + BadgeRewarder.new.reward_top_seven_badges([user.username, user_other.username]) + expect(BadgeAchievement.where(badge_id: badge.id).size).to eq(2) + end end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 4eeedb0b2..78c715ddf 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -210,6 +210,61 @@ RSpec.describe Article, type: :model do end end + describe "queries" do + it "returns article plucked objects that match keyword query" do + create(:search_keyword, + google_result_path: article.path, + google_position: 8, + google_volume: 2000, + google_difficulty: 10) + article.update(featured: true) + articles = Article.seo_boostable + expect(articles.flatten[0]).to eq(article.path) + end + it "returns keyword articles by tag" do + create(:search_keyword, + google_result_path: article.path, + google_position: 8, + google_volume: 2000, + google_difficulty: 10) + article.update(featured: true) + articles = Article.seo_boostable(article.tags.first) + expect(articles.flatten[0]).to eq(article.path) + end + it "does not return articles when none match based on tag" do + create(:search_keyword, + google_result_path: article.path, + google_position: 8, + google_volume: 2000, + google_difficulty: 10) + article.update(featured: true) + articles = Article.seo_boostable("woozle-wozzle-20000") + expect(articles.size).to eq(0) + end + it "does not return keywords that don't match criteria plucked objects that match keyword query" do + create(:search_keyword, + google_result_path: article.path, + google_position: 33, #too high position + google_volume: 2000, + google_difficulty: 10) + articles = Article.seo_boostable + expect(articles.size).to eq(0) + end + it "does not return unpublished articles" do + create(:search_keyword, + google_result_path: article.path, + google_position: 8, + google_volume: 2000, + google_difficulty: 10) + articles = Article.seo_boostable + article.update(published: false) + expect(articles.size).to eq(0) + end + it "returns empty relation if no articles match" do + articles = Article.seo_boostable + expect(articles.size).to eq(0) + end + end it "detects no liquid tag if not used" do expect(article.decorate.liquid_tags_used).to eq([]) @@ -385,4 +440,5 @@ RSpec.describe Article, type: :model do last_year = 1.year.ago.year % 100 expect(article.readable_publish_date.include?("'#{last_year}")).to eq(true) end + end diff --git a/spec/models/search_keyword_spec.rb b/spec/models/search_keyword_spec.rb new file mode 100644 index 000000000..088b06a12 --- /dev/null +++ b/spec/models/search_keyword_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe SearchKeyword, type: :model do + it { is_expected.to validate_presence_of(:keyword) } + it { is_expected.to validate_presence_of(:google_result_path) } + it { is_expected.to validate_presence_of(:google_position) } + it { is_expected.to validate_presence_of(:google_volume) } + it { is_expected.to validate_presence_of(:google_difficulty) } + it { is_expected.to validate_presence_of(:google_checked_at) } + + let(:search_keyword) { create(:search_keyword) } + + it "is valid with proper path" do + search_keyword.google_result_path = "/hello/goodbye" + expect(search_keyword).to be_valid + end + + it "is invalid with improper path" do + search_keyword.google_result_path = "hello/goodbye" + expect(search_keyword).not_to be_valid + end +end