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:
parent
7d64ae8d09
commit
75d3a79c12
10 changed files with 220 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
17
app/models/search_keyword.rb
Normal file
17
app/models/search_keyword.rb
Normal 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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
14
db/migrate/20180624230435_create_search_keywords.rb
Normal file
14
db/migrate/20180624230435_create_search_keywords.rb
Normal 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
|
||||
14
db/schema.rb
14
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"
|
||||
|
|
|
|||
10
spec/factories/search_keywords.rb
Normal file
10
spec/factories/search_keywords.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
spec/models/search_keyword_spec.rb
Normal file
22
spec/models/search_keyword_spec.rb
Normal 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
|
||||
Loading…
Add table
Reference in a new issue