diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index b578cd7..4e020bf 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -14,5 +14,7 @@
// Layouts
// Components
+@import './components/search_stat_details';
+@import './components/raw-response-modal';
// Screens
diff --git a/app/assets/stylesheets/components/_raw-response-modal.scss b/app/assets/stylesheets/components/_raw-response-modal.scss
new file mode 100644
index 0000000..2103b8f
--- /dev/null
+++ b/app/assets/stylesheets/components/_raw-response-modal.scss
@@ -0,0 +1,8 @@
+.raw-response-modal {
+ iframe {
+ width: 100%;
+ height: 80vh;
+
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/components/search_stat_details.scss b/app/assets/stylesheets/components/search_stat_details.scss
new file mode 100644
index 0000000..2e8a5f3
--- /dev/null
+++ b/app/assets/stylesheets/components/search_stat_details.scss
@@ -0,0 +1,5 @@
+.search-stat-detail {
+ &__title::after {
+ content: ':';
+ }
+}
diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb
index 59edf49..02d6557 100644
--- a/app/controllers/search_stats_controller.rb
+++ b/app/controllers/search_stats_controller.rb
@@ -3,6 +3,10 @@
class SearchStatsController < ApplicationController
# GET /search_stats
def index
- @pagy, @search_stats = pagy(SearchStat.all)
+ @pagy, @search_stats = pagy(current_user.search_stats)
+ end
+
+ def show
+ @search_stat = current_user.search_stats.includes(:result_links).find(params[:id])
end
end
diff --git a/app/models/result_link.rb b/app/models/result_link.rb
new file mode 100644
index 0000000..04949c4
--- /dev/null
+++ b/app/models/result_link.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ResultLink < ApplicationRecord
+ enum link_type: { ads_top: 'ads_top', non_ads: 'non_ads' }
+
+ belongs_to :search_stat, inverse_of: :result_links
+
+ validates :url, presence: true
+end
diff --git a/app/models/search_stat.rb b/app/models/search_stat.rb
index 76aad52..283d0a6 100644
--- a/app/models/search_stat.rb
+++ b/app/models/search_stat.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
class SearchStat < ApplicationRecord
- validates :keyword, presence: true
+ has_many :result_links, inverse_of: :search_stat, dependent: :destroy
+ belongs_to :user, inverse_of: :search_stats
+
+ validates :keyword, presence: true, length: { maximum: 255 }
validates :raw_response, presence: true
enum status: { initialized: 0, running: 1, completed: 2, failed: 3 }
diff --git a/app/models/user.rb b/app/models/user.rb
index e6def3e..bacbbce 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -4,6 +4,8 @@ class User < ApplicationRecord
include Authenticable
PASSWORD_PATTERN = /(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/
+ has_many :search_stats, inverse_of: :user, dependent: :destroy
+
before_validation :password_complexity, on: :create
private
diff --git a/app/views/search_stats/_raw_response_modal.html.erb b/app/views/search_stats/_raw_response_modal.html.erb
new file mode 100644
index 0000000..fa92bb8
--- /dev/null
+++ b/app/views/search_stats/_raw_response_modal.html.erb
@@ -0,0 +1,14 @@
+
diff --git a/app/views/search_stats/_search_stat.html.erb b/app/views/search_stats/_search_stat.html.erb
index f0eed2b..1556e46 100644
--- a/app/views/search_stats/_search_stat.html.erb
+++ b/app/views/search_stats/_search_stat.html.erb
@@ -1,31 +1,9 @@
-
-
- <%= SearchStat.human_attribute_name(:keyword) %>:
- <%= search_stat.keyword %>
-
-
-
- <%= SearchStat.human_attribute_name(:ad_count) %>:
- <%= search_stat.ad_count %>
-
-
-
- <%= SearchStat.human_attribute_name(:link_count) %>:
- <%= search_stat.link_count %>
-
-
-
- <%= SearchStat.human_attribute_name(:total_result_count) %>:
- <%= search_stat.total_result_count %>
-
-
-
- <%= SearchStat.human_attribute_name(:raw_response) %>:
- <%= search_stat.raw_response %>
-
-
-
- <%= SearchStat.human_attribute_name(:user_id) %>:
- <%= search_stat.user_id %>
-
-
+
+ | <%= search_stat.keyword %> |
+ <%= search_stat.ad_count %> |
+ <%= search_stat.link_count %> |
+ <%= search_stat.total_result_count %> |
+
+ <%= link_to t('links.search_stat_details'), search_stat_path(search_stat), class: 'btn btn-primary' %>
+ |
+
diff --git a/app/views/search_stats/index.html.erb b/app/views/search_stats/index.html.erb
index 8bb29f5..15e5ffd 100644
--- a/app/views/search_stats/index.html.erb
+++ b/app/views/search_stats/index.html.erb
@@ -1,7 +1,28 @@
-<%= SearchStat.model_name.human %>
+
+
<%= SearchStat.model_name.human %>
-
- <%= render @search_stats %>
-
+ <% if @search_stats.any? %>
+
+
+
+
+ | <%= SearchStat.human_attribute_name(:keyword) %> |
+ <%= SearchStat.human_attribute_name(:ad_count) %> |
+ <%= SearchStat.human_attribute_name(:link_count) %> |
+ <%= SearchStat.human_attribute_name(:total_result_count) %> |
+ |
+
+
+
+ <%= render @search_stats %>
+
+
+
-<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
+ <%== pagy_bootstrap_nav(@pagy) %>
+ <% else %>
+
+
<%= t('titles.no_stats')%>
+
+ <% end %>
+
diff --git a/app/views/search_stats/show.html.erb b/app/views/search_stats/show.html.erb
new file mode 100644
index 0000000..3562b46
--- /dev/null
+++ b/app/views/search_stats/show.html.erb
@@ -0,0 +1,86 @@
+
+
+
+ <%= link_to search_stats_path, class: "btn btn-primary m-1" do %>
+ <%= t('buttons.back') %>
+ <% end %>
+
+
+
+
+
+
<%= SearchStat.model_name.human %>
+
+
+
+ <%= SearchStat.human_attribute_name(:keyword) %>
+ <%= @search_stat.keyword %>
+
+
+
+ <%= SearchStat.human_attribute_name(:ad_count_top) %>
+ <%= @search_stat.top_ad_count %>
+
+
+
+ <%= SearchStat.human_attribute_name(:ad_count) %>
+ <%= @search_stat.ad_count %>
+
+
+
+
+ <%= SearchStat.human_attribute_name(:non_ad_count) %>
+ <%= @search_stat.non_ad_count %>
+
+
+
+ <%= SearchStat.human_attribute_name(:link_count) %>
+ <%= @search_stat.link_count %>
+
+
+
+ <%= SearchStat.human_attribute_name(:total_result_count) %>
+ <%= @search_stat.total_result_count %>
+
+
+
+
+
+ <%= SearchStat.human_attribute_name(:total_result_count) %>:
+ <%= @search_stat.user_id %>
+
+
+
+
+
+
+
+
+
<%=t('titles.top_ads')%>
+
+ <% @search_stat.result_links.ads_top.each do |result_link| %>
+ -
+ <%= link_to result_link.url, result_link.url %>
+
+ <% end %>
+
+
+
+
+
+
+
<%=t('titles.non_ads')%>
+
+ <% @search_stat.result_links.non_ads.each do |result_link| %>
+ -
+ <%= link_to result_link.url, result_link.url, target: '_blank' %>
+
+ <% end %>
+
+
+
+
+ <%= render 'raw_response_modal' %>
+
diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb
new file mode 100644
index 0000000..b2b7b0b
--- /dev/null
+++ b/config/initializers/pagy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'pagy/extras/bootstrap'
+require 'pagy/extras/overflow'
+
+# Override default options
+Pagy::DEFAULT[:items] = 5
+Pagy::DEFAULT[:size] = [1, 2, 2, 1]
+Pagy::DEFAULT[:overflow] = :last_page
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ce17548..6c5868c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -37,8 +37,19 @@ en:
attributes:
search_stat:
ad_count: Ad count
+ ad_count_top: Ad count top
keyword: Keyword
link_count: Link count
+ non_ad_count: Non ad count
raw_response: Raw response
total_result_count: Total result count
user_id: User
+ buttons:
+ back: Back
+ raw_response: View Raw Response
+ links:
+ search_stat_details: Show Details
+ titles:
+ no_stats: No search stats found
+ non_ads: List of URLs of Non-AdWords Results on the Page
+ top_ads: List of URLs of AdWords Advertisers in Top Position
diff --git a/config/routes.rb b/config/routes.rb
index 3df81d3..032b63f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,5 +8,5 @@
root "home#index"
get "/health_check", to: 'health_check#health_check', as: :rails_health_check
- resources :search_stats, only: [:index]
+ resources :search_stats, only: [:index, :show]
end
diff --git a/db/migrate/20230601085414_create_result_links.rb b/db/migrate/20230601085414_create_result_links.rb
new file mode 100644
index 0000000..1b9d9b9
--- /dev/null
+++ b/db/migrate/20230601085414_create_result_links.rb
@@ -0,0 +1,13 @@
+class CreateResultLinks < ActiveRecord::Migration[7.0]
+ def change
+ enable_extension 'citext' unless extension_enabled?('citext')
+
+ create_table :result_links do |t|
+ t.references :search_stat, null: false, foreign_key: true
+ t.integer :link_type, null: false
+ t.citext :url, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20230608032015_change_url_type_in_result_links.rb b/db/migrate/20230608032015_change_url_type_in_result_links.rb
new file mode 100644
index 0000000..e82d677
--- /dev/null
+++ b/db/migrate/20230608032015_change_url_type_in_result_links.rb
@@ -0,0 +1,9 @@
+class ChangeUrlTypeInResultLinks < ActiveRecord::Migration[7.0]
+ def change
+ remove_index :result_links, :url
+
+ change_column :result_links, :url, :string, null: false
+
+ add_index :result_links, :url
+ end
+end
diff --git a/db/migrate/20230612121926_change_link_type_to_string_in_result_links.rb b/db/migrate/20230612121926_change_link_type_to_string_in_result_links.rb
new file mode 100644
index 0000000..06ae3e4
--- /dev/null
+++ b/db/migrate/20230612121926_change_link_type_to_string_in_result_links.rb
@@ -0,0 +1,5 @@
+class ChangeLinkTypeToStringInResultLinks < ActiveRecord::Migration[7.0]
+ def change
+ change_column :result_links, :link_type, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 80dbe34..fd91cf5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,12 +10,51 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_06_02_033657) do
+ActiveRecord::Schema.define(version: 2023_06_15_070454) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "plpgsql"
+ create_table "oauth_access_tokens", force: :cascade do |t|
+ t.bigint "resource_owner_id"
+ t.bigint "application_id", null: false
+ t.string "token", null: false
+ t.string "refresh_token"
+ t.integer "expires_in"
+ t.string "scopes"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "revoked_at", precision: 6
+ t.string "previous_refresh_token", default: "", null: false
+ t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id"
+ t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
+ t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
+ t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
+ end
+
+ create_table "oauth_applications", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "uid", null: false
+ t.string "secret", null: false
+ t.text "redirect_uri"
+ t.string "scopes", default: "", null: false
+ t.boolean "confidential", default: true, null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
+ end
+
+ create_table "result_links", force: :cascade do |t|
+ t.bigint "search_stat_id", null: false
+ t.string "link_type", null: false
+ t.string "url", null: false
+ t.datetime "created_at", precision: 6, default: -> { "CURRENT_TIMESTAMP" }, null: false
+ t.datetime "updated_at", precision: 6, default: -> { "CURRENT_TIMESTAMP" }, null: false
+ t.index ["link_type"], name: "index_result_links_on_link_type"
+ t.index ["search_stat_id"], name: "index_result_links_on_search_stat_id"
+ t.index ["url"], name: "index_result_links_on_url"
+ end
+
create_table "search_stats", force: :cascade do |t|
t.string "keyword", null: false
t.integer "ad_count", default: 0, null: false
@@ -43,5 +82,7 @@
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
+ add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
+ add_foreign_key "result_links", "search_stats"
add_foreign_key "search_stats", "users"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 47549ab..72d0a27 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -9,6 +9,11 @@
require 'fabrication'
# Generate dummy data for SearchStat
-100.times do
- Fabricate(:search_stat)
+
+user = User.find_or_initialize_by(email: 'user@demo.com') do |user|
+ user.password = 'aaaaaaA1'
+end
+
+10.times do
+ Fabricate.times(100, :search_stat, user: user)
end
diff --git a/spec/fabricators/result_link_fabricator.rb b/spec/fabricators/result_link_fabricator.rb
new file mode 100644
index 0000000..9e745b7
--- /dev/null
+++ b/spec/fabricators/result_link_fabricator.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+Fabricator(:result_link) do
+ link_type { ResultLink.link_types.keys.sample }
+ url { FFaker::Internet.http_url }
+end
+
+Fabricator(:result_link_with_search_stat, from: :result_link) do
+ search_stat { Fabricate(:search_stat) }
+end