Add search_keywords record and associated functionality (#494)

* Add search_keywords record and associated functionality

* Fix test and add badge rewarder method
This commit is contained in:
Ben Halpern 2018-06-25 11:58:07 -04:00 committed by GitHub
parent 7d64ae8d09
commit 75d3a79c12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 220 additions and 15 deletions

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -45,6 +45,25 @@
<%= javascript_pack_tag "sidebarWidget", defer: true %>
<div id="sidebarWidget__pack" data-tag-info="<%= @tag_model.attributes.slice("id", "text_color_hex", "bg_color_hex", "name").to_json %>">
</div>
<% else %>
<% cache("seo-boostable-posts-for-tag#{@tag}", :expires_in => 18.hours) do %>
<% @boostable_posts = Article.seo_boostable(@tag) %>
<% if @boostable_posts.any? %>
<div class="widget">
<header>
trending guides/resources
</header>
<div class="widget-body">
<div class="widget-link-list">
<% @boostable_posts.first(8).
each do |plucked_article| %>
<%= render "articles/widget_list_item", plucked_article: plucked_article, show_comment_count: false %>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
<% end %>
<% elsif @query %>
<% elsif @podcast_index %>
@ -185,22 +204,43 @@
<a class="cta cta-button" href="/new/explainlikeimfive">ASK FOR AN EXPLANATION</a>
</div>
</div>
<div class="widget" id="podcast-widget">
<header>
<a href="/pod">latest podcasts</pod>
</header>
<div class="widget-body">
<% @podcast_episodes.each do |ep| %>
<div class="widget-podcast-ep">
<a href="<%= ep.path %>"><%= ep.title %>
<div class="widget-podcast-title">
<a href="<%= ep.podcast.path %>"><%= ep.podcast.title %>
<% if user_signed_in? %>
<div class="widget" id="podcast-widget">
<header>
<a href="/pod">latest podcasts</pod>
</header>
<div class="widget-body">
<% @podcast_episodes.each do |ep| %>
<div class="widget-podcast-ep">
<a href="<%= ep.path %>"><%= ep.title %>
<div class="widget-podcast-title">
<a href="<%= ep.podcast.path %>"><%= ep.podcast.title %>
</div>
</div>
<% end %>
<a class="cta cta-button" href="/pod">VIEW ALL</a>
</div>
</div>
<% else %>
<% cache("seo-boostable-posts-homepage", :expires_in => 18.hours) do %>
<% @boostable_posts = Article.seo_boostable %>
<% if @boostable_posts.any? %>
<div class="widget">
<header>
trending guides/resources
</header>
<div class="widget-body">
<div class="widget-link-list">
<% @boostable_posts.first(12).
each do |plucked_article| %>
<%= render "articles/widget_list_item", plucked_article: plucked_article, show_comment_count: false %>
<% end %>
</div>
</div>
<% end %>
<a class="cta cta-button" href="/pod">VIEW ALL</a>
</div>
</div>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
</div>

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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