Skip to content

Commit a2c4322

Browse files
authored
Merge pull request #46 from suho/release/0.3.0
2 parents ea9bba5 + 864b07c commit a2c4322

29 files changed

Lines changed: 1394 additions & 36 deletions

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ gem 'fabrication' # Fabrication generates objects in Ruby. Fabricators are schem
1313
gem 'sidekiq' # background processing for Ruby
1414
gem 'bootsnap', require: false # Reduces boot times through caching; required in config/boot.rb
1515
gem 'i18n-js', '3.5.1' # A library to provide the I18n translations on the Javascript
16+
gem 'httparty' # A library to call external API
1617

1718
# Authentications & Authorizations
1819
gem 'devise' # Authentication solution for Rails with Warden

Gemfile.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ GEM
198198
globalid (0.5.2)
199199
activesupport (>= 5.0)
200200
hashdiff (1.0.1)
201+
httparty (0.20.0)
202+
mime-types (~> 3.0)
203+
multi_xml (>= 0.5.2)
201204
i18n (1.8.11)
202205
concurrent-ruby (~> 1.0)
203206
i18n-js (3.5.1)
@@ -229,11 +232,15 @@ GEM
229232
marcel (1.0.2)
230233
matrix (0.4.2)
231234
method_source (1.0.0)
235+
mime-types (3.4.1)
236+
mime-types-data (~> 3.2015)
237+
mime-types-data (3.2021.1115)
232238
mini_magick (4.11.0)
233239
mini_mime (1.0.3)
234240
mini_portile2 (2.6.1)
235241
minitest (5.14.4)
236242
msgpack (1.4.2)
243+
multi_xml (0.6.0)
237244
multipart-post (2.1.1)
238245
nap (1.1.0)
239246
nio4r (2.5.8)
@@ -502,6 +509,7 @@ DEPENDENCIES
502509
ffaker
503510
figaro
504511
foreman
512+
httparty
505513
i18n-js (= 3.5.1)
506514
json_matchers
507515
letter_opener

app/controllers/keywords_controller.rb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class KeywordsController < ApplicationController
66
include Pagy::Backend
77

88
def index
9-
pagy, keywords = pagy(current_user.keywords)
9+
pagy, keywords = pagy(current_user.keywords.order('created_at DESC'))
1010
keyword_presenters = keywords.map { |keyword| KeywordPresenter.new(keyword) }
1111

1212
render locals: {
@@ -16,19 +16,22 @@ def index
1616
end
1717

1818
def create
19-
parse_keywords.each do |keyword|
20-
current_user.keywords.create(keyword: keyword)
19+
if save_keywords
20+
SearchKeywordsJob.perform_later(keywords_form.keyword_ids)
21+
flash[:notice] = t('keywords.upload.success')
22+
else
23+
flash[:alert] = keywords_form.errors.full_messages.first
2124
end
2225
redirect_to keywords_path
2326
end
2427

2528
private
2629

27-
def parse_keywords
28-
keywords_file = params[:keywords_file]
29-
ParseKeywordsService.new(keywords_file).call
30-
rescue GoogleSearch::Errors::KeywordsError => e
31-
flash[:alert] = e.message
32-
[]
30+
def save_keywords
31+
keywords_form.save(params[:keywords_file])
32+
end
33+
34+
def keywords_form
35+
@keywords_form ||= KeywordsForm.new(current_user)
3336
end
3437
end

app/forms/keywords_form.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
class KeywordsForm
4+
include ActiveModel::Model
5+
6+
validates_with KeywordsFormValidator
7+
8+
attr_reader :file, :keyword_ids
9+
10+
def initialize(user)
11+
@user = user
12+
end
13+
14+
def save(file)
15+
@file = file
16+
17+
return false if invalid?
18+
19+
begin
20+
keyword_records = parse_keywords.map { |keyword| keyword_record(keyword) }
21+
# rubocop:disable Rails::SkipsModelValidations
22+
@keyword_ids = Keyword.insert_all(keyword_records).map { |keyword| keyword['id'] }
23+
# rubocop:enable Rails::SkipsModelValidations
24+
rescue ActiveRecord::ActiveRecordError
25+
errors.add(:base, I18n.t('keywords.upload.invalid_file'))
26+
end
27+
28+
errors.empty?
29+
end
30+
31+
private
32+
33+
attr_reader :user
34+
35+
def parse_keywords
36+
csv_data = CSV.read(file.path)
37+
csv_data.map(&:first)
38+
end
39+
40+
def keyword_record(keyword)
41+
return nil if keyword.blank?
42+
43+
{
44+
user_id: user.id,
45+
keyword: keyword,
46+
created_at: Time.current,
47+
updated_at: Time.current
48+
}
49+
end
50+
end

app/jobs/search_keyword_job.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
class SearchKeywordJob < ApplicationJob
4+
queue_as :default
5+
6+
def perform(keyword_id)
7+
keyword = Keyword.find(keyword_id)
8+
html = GoogleSearchService.new(keyword: keyword.keyword).call
9+
utf_8_html = html.force_encoding('iso8859-1').encode('utf-8')
10+
keyword.add_html(utf_8_html)
11+
rescue ActiveRecord::RecordNotFound, GoogleSearch::Errors::SearchKeywordError, ActiveRecord::StatementInvalid
12+
keyword.update_status(:failed)
13+
end
14+
end

app/jobs/search_keywords_job.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class SearchKeywordsJob < ApplicationJob
4+
queue_as :default
5+
6+
def perform(keyword_ids)
7+
keyword_ids.each_with_index do |keyword_id, index|
8+
SearchKeywordJob.set(wait: 1 + (index * 2)).perform_later(keyword_id)
9+
end
10+
end
11+
end

app/lib/google_search/errors.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@
33
module GoogleSearch
44
module Errors
55
class KeywordsError < StandardError; end
6+
7+
class SearchKeywordError < StandardError; end
68
end
79
end

app/models/keyword.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,15 @@
33
class Keyword < ApplicationRecord
44
belongs_to :user
55

6+
validates :keyword, presence: true, length: { maximum: 255 }
7+
68
enum status: { in_progress: 0, completed: 1, failed: 2 }
9+
10+
def update_status(status)
11+
update(status: status)
12+
end
13+
14+
def add_html(html)
15+
update(html: html, status: :completed)
16+
end
717
end

app/presenters/keyword_presenter.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ def keyword_text
1010
end
1111

1212
def formatted_created_at
13-
keyword.created_at.strftime('%F')
13+
keyword.created_at.strftime('%F %H:%M:%S')
1414
end
1515

1616
def status_html
1717
return '<div class="spinner-border spinner-border-sm" role="status"></div>' if keyword.in_progress?
18-
return '<p class="text-success">Completed</p>' if keyword.completed?
19-
return '<p class="text-danger">Failed</p>' if keyword.failed?
18+
return '<div class="text-success">Completed</div>' if keyword.completed?
19+
return '<div class="text-danger">Failed</div>' if keyword.failed?
2020
end
2121

2222
private
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
class GoogleSearchService
4+
BASE_URL = 'https://www.google.com/search'
5+
6+
def initialize(keyword:)
7+
@uri = URI("#{BASE_URL}?q=#{CGI.escape(keyword)}")
8+
@user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) '\
9+
'Version/11.1.2 Safari/605.1.15'
10+
end
11+
12+
def call
13+
data = HTTParty.get(@uri, { headers: { 'User-Agent' => user_agent } })
14+
raise GoogleSearch::Errors::SearchKeywordError unless data.response.code == '200'
15+
16+
data
17+
end
18+
19+
private
20+
21+
attr_reader :uri, :user_agent
22+
end

0 commit comments

Comments
 (0)