Skip to content

Commit

Permalink
Merge pull request #53 from malparty/release/0.4.0
Browse files Browse the repository at this point in the history
Release - 0.4.0
  • Loading branch information
malparty committed Jul 2, 2021
2 parents d575ab4 + 86d7c69 commit 8c274b7
Show file tree
Hide file tree
Showing 39 changed files with 4,490 additions and 63 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Expand Up @@ -7,14 +7,15 @@ gem 'pg' # Use Postgresql as database
gem 'puma' # Use Puma as the app server
gem 'mini_magick' # A ruby wrapper for ImageMagick or GraphicsMagick command line
gem 'pagy' # A pagination gem that is very light and fast
gem 'paranoia' # Paranoia is a re-implementation of acts_as_paranoid for Rails 3 and Rails 4. Soft-deletion of records
gem 'discard' # Soft deletes for ActiveRecord
gem 'ffaker' # A library for generating fake data such as names, addresses, and phone numbers.
gem 'fabrication' # Fabrication generates objects in Ruby. Fabricators are schematics for your objects, and can be created as needed anywhere in your app or specs.
gem 'sidekiq' # background processing for Ruby
gem 'bootsnap', require: false # Reduces boot times through caching; required in config/boot.rb
gem 'i18n-js', '3.5.1' # A library to provide the I18n translations on the Javascript
gem 'jsonapi-serializer' # A fast JSON:API serializer for Ruby Objects.
gem 'httparty' # A HTTP client for Ruby.
gem 'nokogiri' # Nokogiri makes it easy and painless to work with XML and HTML from Ruby

# Authentications & Authorizations
gem 'devise' # Authentication solution for Rails with Warden
Expand Down
7 changes: 4 additions & 3 deletions Gemfile.lock
Expand Up @@ -157,6 +157,8 @@ GEM
responders
warden (~> 1.2.3)
diff-lcs (1.4.4)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.4.0)
doorkeeper (5.5.1)
railties (>= 5)
Expand Down Expand Up @@ -247,8 +249,6 @@ GEM
orm_adapter (0.5.0)
pagy (3.13.0)
parallel (1.20.1)
paranoia (2.4.3)
activerecord (>= 4.0, < 6.2)
parser (3.0.1.1)
ast (~> 2.4.1)
pg (1.2.3)
Expand Down Expand Up @@ -493,6 +493,7 @@ DEPENDENCIES
danger-undercover
database_cleaner
devise
discard
doorkeeper
fabrication
ffaker
Expand All @@ -505,8 +506,8 @@ DEPENDENCIES
letter_opener
listen (= 3.1.5)
mini_magick
nokogiri
pagy
paranoia
pg
pry-byebug
pry-rails
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Expand Up @@ -15,5 +15,6 @@
@import 'layouts/default';

// Components
@import 'components/list_keywords_group';

// Screens
6 changes: 6 additions & 0 deletions app/assets/stylesheets/components/_list_keywords_group.scss
@@ -0,0 +1,6 @@
.list-keyword-group {
&__header {
min-width: 3em;
color: $gray-500;
}
}
16 changes: 15 additions & 1 deletion app/controllers/keywords_controller.rb
@@ -1,5 +1,19 @@
# frozen_string_literal: true

class KeywordsController < ApplicationController
def index; end
include Pagy::Backend

def index
pagy, keywords_list = pagy(keywords)

render locals: {
pagy: pagy, keywords: KeywordsCollectionPresenter.new(keywords_list)
}
end

private

def keywords
KeywordsQuery.new(current_user).call
end
end
16 changes: 16 additions & 0 deletions app/controllers/registrations_controller.rb
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class RegistrationsController < Devise::RegistrationsController
# Override user hard-delete (from Devise) with user soft-delete
def destroy
resource.discard

Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)

set_flash_message :notice, :destroyed

yield resource if block_given?

respond_with_navigational(resource) { redirect_to after_sign_out_path_for(resource_name) }
end
end
1 change: 1 addition & 0 deletions app/helpers/application_helper.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true

module ApplicationHelper
include Pagy::Frontend
end
11 changes: 11 additions & 0 deletions app/models/keyword.rb
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class Keyword < ApplicationRecord
include Discard::Model

validates :name, presence: true, length: { maximum: 255 }

belongs_to :user, inverse_of: :keywords

default_scope -> { kept }
end
6 changes: 6 additions & 0 deletions app/models/user.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true

class User < ApplicationRecord
include Discard::Model

has_many :keywords, inverse_of: :user, dependent: :destroy

default_scope -> { kept }

# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
Expand Down
15 changes: 15 additions & 0 deletions app/presenters/keywords_collection_presenter.rb
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class KeywordsCollectionPresenter
def initialize(keywords)
@keywords = keywords
end

def groups
keywords.group_by { |keyword| keyword.name[0].upcase.to_sym }
end

private

attr_reader :keywords
end
19 changes: 19 additions & 0 deletions app/queries/keywords_query.rb
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class KeywordsQuery
def initialize(user)
@keywords = user.keywords
end

def call
order_by_name
end

private

attr_reader :keywords

def order_by_name
keywords.order(:name)
end
end
69 changes: 69 additions & 0 deletions app/services/google/parser_service.rb
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module Google
class ParserService
NON_ADS_RESULT_SELECTOR = 'a[data-ved]:not([role]):not([jsaction]):not(.adwords):not(.footer-links)'
AD_CONTAINER_ID = 'tads'
ADWORDS_CLASS = 'adwords'

def initialize(html_response:)
raise ArgumentError, 'response.body cannot be blank' if html_response.body.blank?

@html = html_response

@document = Nokogiri::HTML.parse(html_response)

# Add a class to all AdWords link for easier manipulation
document.css('div[data-text-ad] a[data-ved]').add_class(ADWORDS_CLASS)

# Mark footer links to identify them
document.css('#footcnt a').add_class('footer-links')
end

# Parse html data and return a hash with the results
def call
{
ads_top_count: ads_top_count,
ads_page_count: ads_page_count,
ads_top_url: ads_top_url,
ads_page_url: ads_page_url,
non_ads_result_count: non_ads_result_count,
non_ads_url: non_ads_url,
total_link_count: total_link_count,
html: html
}
end

private

attr_reader :html, :document

def ads_top_count
document.css("##{AD_CONTAINER_ID} .#{ADWORDS_CLASS}").count
end

def ads_page_count
document.css(".#{ADWORDS_CLASS}").count
end

def ads_top_url
document.css("##{AD_CONTAINER_ID} .#{ADWORDS_CLASS}").map { |a_tag| a_tag['href'] }
end

def ads_page_url
document.css(".#{ADWORDS_CLASS}").map { |a_tag| a_tag['href'] }
end

def non_ads_result_count
document.css(NON_ADS_RESULT_SELECTOR).count
end

def non_ads_url
document.css(NON_ADS_RESULT_SELECTOR).map { |a_tag| a_tag['href'] }
end

def total_link_count
document.css('a').count
end
end
end
32 changes: 20 additions & 12 deletions app/views/devise/passwords/new.html.erb
@@ -1,17 +1,25 @@
<h2>Forgot your password?</h2>
<div class="container">
<div class="row justify-content-center">
<div class="col col-md-8 col-lg-6">
<div class="card">
<section class="card-body">
<h2><%= t('auth.forgot_password') %></h2>

<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= render 'devise/shared/error_messages', resource: resource %>

<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<div class="form-group">
<%= f.label :email %><br/>
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'form-control' %>
</div>

<div class="actions">
<%= f.submit "Send me reset password instructions" %>
</div>
<% end %>
<%= f.submit t('auth.btn_reset_password'), class: 'btn btn-primary btn-block' %>
<% end %>
<%= render "devise/shared/links" %>
<%= render 'devise/shared/links' %>
</section>
</div>
</div>
</div>
</div>
12 changes: 12 additions & 0 deletions app/views/keywords/_list_keywords.html.erb
@@ -0,0 +1,12 @@
<section class="list-keyword">
<% if keywords.groups.any? %>
<% keywords.groups.each do |group_key, group_keywords| %>
<%= render 'list_keywords_group', group_key: group_key, group_keywords: group_keywords %>
<% end %>
<div class="d-flex justify-content-around">
<%== pagy_bootstrap_nav(pagy) %>
</div>
<% else %>
<div class="alert alert-light"><%= t('keywords.empty_list') %></div>
<% end %>
</section>
13 changes: 13 additions & 0 deletions app/views/keywords/_list_keywords_group.html.erb
@@ -0,0 +1,13 @@
<div class="d-flex flex-row list-keyword-group mb-2">
<div class="list-keyword-group__header">
<h2 class="mb-0">
<%= group_key %>
</h2>
</div>
<ul class="list-inline mb-0">
<% group_keywords.each do |keyword| %>
<li class="list-inline-item list-keyword-item"><%= keyword.name %></li>
<% end %>
</ul>
</div>
<hr/>
5 changes: 3 additions & 2 deletions app/views/keywords/index.html.erb
@@ -1,2 +1,3 @@
<h1>Keywords#index</h1>
<p>Find me in app/views/keyword/index.html.erb</p>
<div class="container">
<%= render 'list_keywords', keywords: keywords, pagy: pagy %>
</div>
2 changes: 1 addition & 1 deletion config/initializers/devise.rb
Expand Up @@ -90,7 +90,7 @@
# It will change confirmation, password recovery and other workflows
# to behave the same regardless if the e-mail provided was right or wrong.
# Does not affect registerable.
# config.paranoid = true
config.paranoid = true

# By default Devise will store the user in session. You can skip storage for
# particular strategies by setting this option.
Expand Down

0 comments on commit 8c274b7

Please sign in to comment.