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? %> +
+ + + + + + + + + + + + <%= render @search_stats %> + +
<%= SearchStat.human_attribute_name(:keyword) %><%= SearchStat.human_attribute_name(:ad_count) %><%= SearchStat.human_attribute_name(:link_count) %><%= SearchStat.human_attribute_name(:total_result_count) %>
+
-<%== 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