From 33296d131f21078f6aca1a3f00086b91e955c8f3 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 11:30:28 +0700 Subject: [PATCH 01/54] Add home page --- app/controllers/home_controller.rb | 5 +++++ app/views/home/index.html.erb | 3 +++ config/routes.rb | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 app/controllers/home_controller.rb create mode 100644 app/views/home/index.html.erb diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..0611fde --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,5 @@ +class HomeController < ApplicationController + def index + render + end +end \ No newline at end of file diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..334b64b --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,3 @@ +

+ Welcome to the world of scrapping!! +

\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 8a3ba68..3565537 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ # Defines the root path route ("/") # root "articles#index" - root "rails/welcome#index" + root "home#index" get "/health_check", to: 'health_check#health_check', as: :rails_health_check end From 7ad64d23c60b8343f521454d1aa325f19caff9e2 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 11:50:04 +0700 Subject: [PATCH 02/54] Add devise gem --- Gemfile | 1 + Gemfile.lock | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Gemfile b/Gemfile index a0c6d8e..1c6fdf7 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,7 @@ gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] # Windows doe # gem 'redis' # Use Redis adapter to run Action Cable in production # gem 'kredis' # Use Kredis to get higher-level data types in Redis # gem 'bcrypt' # Use Active Model has_secure_password +gem 'devise' # Authentications & Authorizations gem 'pundit' # Minimal authorization through OO design and pure Ruby classes diff --git a/Gemfile.lock b/Gemfile.lock index 4436443..01dce6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,7 @@ GEM public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) awesome_print (1.9.2) + bcrypt (3.1.18) better_errors (2.10.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -154,6 +155,12 @@ GEM database_cleaner-core (2.0.1) date (3.3.3) debug_inspector (1.1.0) + devise (4.9.2) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) diff-lcs (1.5.0) discard (1.2.1) activerecord (>= 4.2, < 8) @@ -239,6 +246,7 @@ GEM faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) + orm_adapter (0.5.0) pagy (6.0.4) parallel (1.23.0) parser (3.0.1.1) @@ -305,6 +313,9 @@ GEM parser (~> 3.0.0) rainbow (>= 2.0, < 4.0) regexp_parser (2.8.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) roadie (5.1.0) css_parser (~> 1.4) @@ -409,6 +420,8 @@ GEM unicode-display_width (2.4.2) uniform_notifier (1.16.0) vcr (6.1.0) + warden (1.2.9) + rack (>= 2.0.9) webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) @@ -448,6 +461,7 @@ DEPENDENCIES danger-suggester danger-undercover database_cleaner + devise discard dockerfile-rails (>= 1.2) fabrication From 63209d94c4405ff10c23f3a5012d0c7bd3a42887 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 11:50:20 +0700 Subject: [PATCH 03/54] Add config files for devis gem --- config/initializers/devise.rb | 315 ++++++++++++++++++++++++++++++++++ config/locales/devise.en.yml | 65 +++++++ 2 files changed, 380 insertions(+) create mode 100644 config/initializers/devise.rb create mode 100644 config/locales/devise.en.yml diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..eaa3521 --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,315 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '898eb58edd14a1a6951f6233c584069dde1f0d29d43b29dd5af4d9d4f8c0764027881a54b3bd6dae53d4ede12de33728bd05eb06ab65cdd0c1bb2e595b58d0b9' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # 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 + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '45b762edc0d8ab4bac2607f6e0c06879cc5b0b7ce90b7b59db6ed40c6a2aef9462c136471da1c787df7b373c12c1f9d50d9034550ac3b41b11b2eed25dc5058f' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true + + config.navigational_formats = ['*/*', :html, :turbo_stream] +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000..260e1c4 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" From a941293c95a4ce864616a012d765b309afb9f84d Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 11:50:49 +0700 Subject: [PATCH 04/54] Add alert in layout application --- app/views/layouts/application.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2d18ae1..bcf85f8 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,8 @@ +

<%= notice %>

+

<%= alert %>

<%= yield %> From f08d8da56d76b97f20254fb9661d38a684b5c8b5 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 12:20:37 +0700 Subject: [PATCH 05/54] Add basic user registration flow using devise gem --- app/models/user.rb | 6 +++ config/routes.rb | 5 ++- .../20230519045115_devise_create_users.rb | 44 +++++++++++++++++++ db/schema.rb | 14 +++++- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20230519045115_devise_create_users.rb diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..4756799 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/config/routes.rb b/config/routes.rb index 3565537..41b0a5a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,11 @@ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - + devise_for :users + # Defines the root path route ("/") # root "articles#index" root "home#index" - + get "/health_check", to: 'health_check#health_check', as: :rails_health_check end diff --git a/db/migrate/20230519045115_devise_create_users.rb b/db/migrate/20230519045115_devise_create_users.rb new file mode 100644 index 0000000..43927db --- /dev/null +++ b/db/migrate/20230519045115_devise_create_users.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[7.0] + def change + create_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 4603022..9434751 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,21 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 2023_05_19_045115) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at", precision: 6 + t.datetime "remember_created_at", precision: 6 + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end + end From dc0a701040cc3a2b2b83774846d3a18288ba5306 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 14:29:51 +0700 Subject: [PATCH 06/54] Show welcome message to signed in users --- app/views/home/index.html.erb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 334b64b..4c23d97 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -1,3 +1,9 @@ +<% if user_signed_in? %> +
Welcome <%= current_user.email %>
+ <%= button_to "Sign out", destroy_user_session_path, method: :delete %> +<% else %> + <%= button_to "Sign in", new_user_session_path %> +<% end %>

Welcome to the world of scrapping!!

\ No newline at end of file From f767e8fd0b6407ea30aca5b48e1d6aadf062e8f2 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 14:31:01 +0700 Subject: [PATCH 07/54] Add password complexity validation for user signup --- app/models/user.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 4756799..18cc688 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,21 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + # Other Devise configurations + + validate :password_complexity, if: :password_required? + + def password_complexity + return if password.blank? || password.match?(/(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/) + + errors.add(:password, 'must include at least one uppercase letter, one lowercase letter, and one digit') + errors.add(:password, 'must be at least 8 characters long') if password.length < 8 + end + + private + + def password_required? + !persisted? || !password.nil? || !password_confirmation.nil? + end end From 183d24919f820d42b43a424aa6871d155822d422 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 14:53:27 +0700 Subject: [PATCH 08/54] Change minimum password requirement from 6 to 8 in devise config --- config/initializers/devise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index eaa3521..52379fc 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -178,7 +178,7 @@ # ==> Configuration for :validatable # Range for password length. - config.password_length = 6..128 + config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly From 7539e8bea19886c50aaf129c8b47c90eab4e4239 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 14:54:26 +0700 Subject: [PATCH 09/54] Remove redundant password length error --- app/models/user.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 18cc688..a85842f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,7 +12,6 @@ def password_complexity return if password.blank? || password.match?(/(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/) errors.add(:password, 'must include at least one uppercase letter, one lowercase letter, and one digit') - errors.add(:password, 'must be at least 8 characters long') if password.length < 8 end private From a72413f57f6830ef6cec5d8e5a619039e4059a9c Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 15:15:59 +0700 Subject: [PATCH 10/54] Fix lint --- app/controllers/home_controller.rb | 10 ++++++---- app/models/user.rb | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 0611fde..3352b9c 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class HomeController < ApplicationController - def index - render - end -end \ No newline at end of file + def index + render + end +end diff --git a/app/models/user.rb b/app/models/user.rb index a85842f..3c3760c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable From 53cc4fffb019617f8b4e383c2cba657c2ef140f9 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 15:21:20 +0700 Subject: [PATCH 11/54] Change branch name for fly deployment --- .github/workflows/deploy_fly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/deploy_fly.yml b/.github/workflows/deploy_fly.yml index 72eb102..d8b681a 100644 --- a/.github/workflows/deploy_fly.yml +++ b/.github/workflows/deploy_fly.yml @@ -3,8 +3,7 @@ on: push: branches: - main - - development - - chore/setup-cicd + - develop jobs: deploy: name: Deploy app From 3acfcb6aa80fb5f6f562e73fcb596099dac0c9d2 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 19 May 2023 15:39:46 +0700 Subject: [PATCH 12/54] Remove nil check from password confirmation validation --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 3c3760c..f059a50 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,6 @@ def password_complexity private def password_required? - !persisted? || !password.nil? || !password_confirmation.nil? + !persisted? || password.present? || password_confirmation.present? end end From 5ca8dce602850deacc9a56fa0ee2c9894bb427a7 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 14:25:42 +0700 Subject: [PATCH 13/54] Add github co owners file --- .github/CODEOWNERS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3430c88 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in the repo. + +# Team Reviewer +* @topnimble @mosharaf13 @longnd @sanG-github From 60021d965cf3f25c9ddb825eed2c91f0f1e4114f Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 14:29:29 +0700 Subject: [PATCH 14/54] Update Gemfile Co-authored-by: Long Nguyen --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 1c6fdf7..72d2ea1 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] # Windows doe # gem 'redis' # Use Redis adapter to run Action Cable in production # gem 'kredis' # Use Kredis to get higher-level data types in Redis # gem 'bcrypt' # Use Active Model has_secure_password -gem 'devise' +gem 'devise' # Flexible authentication solution for Rails with Warden # Authentications & Authorizations gem 'pundit' # Minimal authorization through OO design and pure Ruby classes From ab783dbc0cc1cae1171ce1d2cefc02b4ce80fe8a Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 14:50:09 +0700 Subject: [PATCH 15/54] Change user email type to citext --- .../20230522073804_change_email_data_type_in_users.rb | 9 +++++++++ db/schema.rb | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20230522073804_change_email_data_type_in_users.rb diff --git a/db/migrate/20230522073804_change_email_data_type_in_users.rb b/db/migrate/20230522073804_change_email_data_type_in_users.rb new file mode 100644 index 0000000..9b2b977 --- /dev/null +++ b/db/migrate/20230522073804_change_email_data_type_in_users.rb @@ -0,0 +1,9 @@ +class ChangeEmailDataTypeInUsers < ActiveRecord::Migration[6.0] + def up + change_column :users, :email, :citext, default: "", null: false + end + + def down + change_column :users, :email, :string, default: "", null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9434751..ff9541e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,13 +10,14 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_05_19_045115) do +ActiveRecord::Schema.define(version: 2023_05_22_073804) do # These are extensions that must be enabled in order to support this database + enable_extension "citext" enable_extension "plpgsql" create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false + t.citext "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at", precision: 6 From a22fac38ab817b686dd423653a3440a319098236 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 15:47:38 +0700 Subject: [PATCH 16/54] Extract devise configs as a authenticable module for user model --- app/models/concerns/authenticable.rb | 11 +++++++++++ app/models/user.rb | 13 +------------ 2 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 app/models/concerns/authenticable.rb diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb new file mode 100644 index 0000000..52beb4d --- /dev/null +++ b/app/models/concerns/authenticable.rb @@ -0,0 +1,11 @@ +module Authenticable + extend ActiveSupport::Concern + + included do + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validate :password_complexity, if: :password_required? + end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index f059a50..d90f8b4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,14 +1,8 @@ # frozen_string_literal: true class User < ApplicationRecord - # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable - # Other Devise configurations - - validate :password_complexity, if: :password_required? + include Authenticable def password_complexity return if password.blank? || password.match?(/(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/) @@ -16,9 +10,4 @@ def password_complexity errors.add(:password, 'must include at least one uppercase letter, one lowercase letter, and one digit') end - private - - def password_required? - !persisted? || password.present? || password_confirmation.present? - end end From c41b8f65e385a8bf7a93c2320ddb4039d6b9eaa4 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 16:09:20 +0700 Subject: [PATCH 17/54] Extract authenticable module from user model --- app/models/concerns/authenticable.rb | 4 +++- app/models/user.rb | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb index 52beb4d..a6dcc01 100644 --- a/app/models/concerns/authenticable.rb +++ b/app/models/concerns/authenticable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Authenticable extend ActiveSupport::Concern @@ -8,4 +10,4 @@ module Authenticable validate :password_complexity, if: :password_required? end -end \ No newline at end of file +end diff --git a/app/models/user.rb b/app/models/user.rb index d90f8b4..bc9cbc3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class User < ApplicationRecord - include Authenticable + PASSWORD_PATTERN = /(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/ def password_complexity - return if password.blank? || password.match?(/(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}/) + return if password.match?(PASSWORD_PATTERN) - errors.add(:password, 'must include at least one uppercase letter, one lowercase letter, and one digit') + errors.add(:password, :invalid) end - end From 0b86c775d76fcc33242fba3e80268a9b6a1fa4b0 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 16:39:28 +0700 Subject: [PATCH 18/54] Run password complexity validation on user create hook --- app/models/concerns/authenticable.rb | 2 -- app/models/user.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb index a6dcc01..b556429 100644 --- a/app/models/concerns/authenticable.rb +++ b/app/models/concerns/authenticable.rb @@ -7,7 +7,5 @@ module Authenticable # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable - - validate :password_complexity, if: :password_required? end end diff --git a/app/models/user.rb b/app/models/user.rb index bc9cbc3..351e132 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,}/ + before_validation :password_complexity, on: :create + def password_complexity return if password.match?(PASSWORD_PATTERN) From 5e75a506ceef40e60dfc57b228a8a2f5b79a9268 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Mon, 22 May 2023 16:39:36 +0700 Subject: [PATCH 19/54] Fix indent --- app/views/home/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 4c23d97..4402bd8 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -5,5 +5,5 @@ <%= button_to "Sign in", new_user_session_path %> <% end %>

- Welcome to the world of scrapping!! + Welcome to the world of scrapping!!

\ No newline at end of file From db8150dfddf2c73f945256e4a6e0869242d07b2d Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Tue, 23 May 2023 10:11:17 +0700 Subject: [PATCH 20/54] Enable and disable "citext" extension on migration --- db/migrate/20230522073804_change_email_data_type_in_users.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20230522073804_change_email_data_type_in_users.rb b/db/migrate/20230522073804_change_email_data_type_in_users.rb index 9b2b977..05304b8 100644 --- a/db/migrate/20230522073804_change_email_data_type_in_users.rb +++ b/db/migrate/20230522073804_change_email_data_type_in_users.rb @@ -1,9 +1,11 @@ class ChangeEmailDataTypeInUsers < ActiveRecord::Migration[6.0] def up + enable_extension("citext") change_column :users, :email, :citext, default: "", null: false end def down change_column :users, :email, :string, default: "", null: false + disable_extension("citext") end end From 6158bf18611f51416c876c01d80e3622dbb1282f Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Thu, 25 May 2023 17:33:15 +0700 Subject: [PATCH 21/54] Remove render from home controller index Co-authored-by: Sang Huynh Thanh <63148598+sanG-github@users.noreply.github.com> --- app/controllers/home_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3352b9c..a0ac1f8 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class HomeController < ApplicationController - def index - render - end + def index; end end From 5ea7ec48e2c735f245c57de5403c9555a75a31fd Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 26 May 2023 09:52:48 +0700 Subject: [PATCH 22/54] Make password complexity checking private Co-authored-by: Sang Huynh Thanh <63148598+sanG-github@users.noreply.github.com> --- app/models/user.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 351e132..1bc6079 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,6 +6,8 @@ class User < ApplicationRecord before_validation :password_complexity, on: :create + private + def password_complexity return if password.match?(PASSWORD_PATTERN) From fe8c19954dc01ee0b015b5013c0f60223ee5a3e5 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 26 May 2023 09:53:52 +0700 Subject: [PATCH 23/54] Update app/views/home/index.html.erb Co-authored-by: Sang Huynh Thanh <63148598+sanG-github@users.noreply.github.com> --- app/views/home/index.html.erb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 4402bd8..d9829e7 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -1,9 +1,8 @@ <% if user_signed_in? %> -
Welcome <%= current_user.email %>
+
Welcome <%= current_user.email %>
<%= button_to "Sign out", destroy_user_session_path, method: :delete %> <% else %> <%= button_to "Sign in", new_user_session_path %> <% end %> -

- Welcome to the world of scrapping!! -

\ No newline at end of file + +

Welcome to the world of scrapping!!

From 1213306c1500af871f48311339abe49313b23308 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 26 May 2023 10:51:26 +0700 Subject: [PATCH 24/54] Fix lint --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1bc6079..e6def3e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,7 +7,7 @@ class User < ApplicationRecord before_validation :password_complexity, on: :create private - + def password_complexity return if password.match?(PASSWORD_PATTERN) From 27e7f72ef0739f93b187695ecac0fcfc56b5b351 Mon Sep 17 00:00:00 2001 From: Md Mosharaf Hossan Date: Fri, 26 May 2023 14:48:00 +0700 Subject: [PATCH 25/54] Update config/routes.rb Co-authored-by: Sang Huynh Thanh <63148598+sanG-github@users.noreply.github.com> --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 41b0a5a..2c6d88c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,6 @@ # Defines the root path route ("/") # root "articles#index" root "home#index" - + get "/health_check", to: 'health_check#health_check', as: :rails_health_check end From f095c0a6b407e3b5c628afd22ef7913edd1ee9a9 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Tue, 30 May 2023 14:05:39 +0700 Subject: [PATCH 26/54] Upload file --- app/controllers/search_stats_controller.rb | 15 +++++++++++++++ app/views/search_stats/new.html.erb | 4 ++++ config/routes.rb | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 app/controllers/search_stats_controller.rb create mode 100644 app/views/search_stats/new.html.erb diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb new file mode 100644 index 0000000..b83f654 --- /dev/null +++ b/app/controllers/search_stats_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class SearchStatsController < ApplicationController + def new + search_stat = SearchStat.new + + render :new, locals: { + search_stat: @search_stat + } + end + + def create + uploaded_file = params[:csv_file] + end +end diff --git a/app/views/search_stats/new.html.erb b/app/views/search_stats/new.html.erb new file mode 100644 index 0000000..55c5b9c --- /dev/null +++ b/app/views/search_stats/new.html.erb @@ -0,0 +1,4 @@ +<%= form_with url: "/uploads", multipart: true do |form| %> + <%= file_field_tag :csv_file %> + <%= form.submit "Upload" %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 2c6d88c..9a84eac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,5 +7,7 @@ # root "articles#index" root "home#index" + resources :search_stats, only: [:new, :create] + get "/health_check", to: 'health_check#health_check', as: :rails_health_check end From 10658458f0f8fe3dc05e27fd917bdc610058608f Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Tue, 30 May 2023 17:02:32 +0700 Subject: [PATCH 27/54] Insert to database --- app/controllers/search_stats_controller.rb | 14 ++++++++++---- app/models/search_stat.rb | 4 ++++ app/views/search_stats/new.html.erb | 2 +- config/routes.rb | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 app/models/search_stat.rb diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index b83f654..b2745dc 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -1,15 +1,21 @@ # frozen_string_literal: true +require 'csv' + class SearchStatsController < ApplicationController def new search_stat = SearchStat.new - render :new, locals: { - search_stat: @search_stat - } + render :new, locals: { search_stat: search_stat } end def create - uploaded_file = params[:csv_file] + csv_file_content = params[:csv_file].read + + keywords = CSV.parse(csv_file_content).flatten + + keywords.map do |keyword| + SearchStat.insert({ keyword: keyword }) + end end end diff --git a/app/models/search_stat.rb b/app/models/search_stat.rb new file mode 100644 index 0000000..f7835bf --- /dev/null +++ b/app/models/search_stat.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class SearchStat < ApplicationRecord +end diff --git a/app/views/search_stats/new.html.erb b/app/views/search_stats/new.html.erb index 55c5b9c..c35b836 100644 --- a/app/views/search_stats/new.html.erb +++ b/app/views/search_stats/new.html.erb @@ -1,4 +1,4 @@ -<%= form_with url: "/uploads", multipart: true do |form| %> +<%= form_with url: "/search_stats", multipart: true do |form| %> <%= file_field_tag :csv_file %> <%= form.submit "Upload" %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 9a84eac..e6338f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,7 @@ # root "articles#index" root "home#index" - resources :search_stats, only: [:new, :create] + resources :search_stats get "/health_check", to: 'health_check#health_check', as: :rails_health_check end From 5f732a96c30cc8ab0a71cec5e50ade73cf3b5d99 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Tue, 30 May 2023 17:13:22 +0700 Subject: [PATCH 28/54] Add Bootstrap classes --- app/views/search_stats/new.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/search_stats/new.html.erb b/app/views/search_stats/new.html.erb index c35b836..a8b697c 100644 --- a/app/views/search_stats/new.html.erb +++ b/app/views/search_stats/new.html.erb @@ -1,4 +1,4 @@ <%= form_with url: "/search_stats", multipart: true do |form| %> - <%= file_field_tag :csv_file %> - <%= form.submit "Upload" %> + <%= file_field_tag :csv_file, class: "form-control" %> + <%= form.submit "Upload", class: "btn btn-primary" %> <% end %> From 46d87d2bff962ace0c316ca84c0e002da52bfea5 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Tue, 30 May 2023 23:23:29 +0700 Subject: [PATCH 29/54] Add validation --- app/models/search_stat.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/search_stat.rb b/app/models/search_stat.rb index f7835bf..b6ee326 100644 --- a/app/models/search_stat.rb +++ b/app/models/search_stat.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true class SearchStat < ApplicationRecord + validates :keyword, presence: true end From 70b5a14b3410de7eaf5334124cba4ba1ddd7e5c2 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 02:38:03 +0700 Subject: [PATCH 30/54] Add helpers --- spec/fabricators/user_fabricator.rb | 6 ++++++ spec/support/user_authentication.rb | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 spec/fabricators/user_fabricator.rb create mode 100644 spec/support/user_authentication.rb diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb new file mode 100644 index 0000000..8dc4dbe --- /dev/null +++ b/spec/fabricators/user_fabricator.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +Fabricator(:user) do + email { FFaker::Internet.email } + password { FFaker::Internet.password } +end diff --git a/spec/support/user_authentication.rb b/spec/support/user_authentication.rb new file mode 100644 index 0000000..9f7080d --- /dev/null +++ b/spec/support/user_authentication.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +def login_as_user(user = Fabricate(:user)) + login_as user +end From b48ad3b61c7fb64705093e57a6a0df92fa2db462 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 11:12:00 +0700 Subject: [PATCH 31/54] Add fixture files --- spec/fixtures/files/empty.csv | 0 spec/fixtures/files/invalid.csv | 3 +++ spec/fixtures/files/keywords.csv | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 spec/fixtures/files/empty.csv create mode 100644 spec/fixtures/files/invalid.csv create mode 100644 spec/fixtures/files/keywords.csv diff --git a/spec/fixtures/files/empty.csv b/spec/fixtures/files/empty.csv new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/files/invalid.csv b/spec/fixtures/files/invalid.csv new file mode 100644 index 0000000..b7ec413 --- /dev/null +++ b/spec/fixtures/files/invalid.csv @@ -0,0 +1,3 @@ + + + diff --git a/spec/fixtures/files/keywords.csv b/spec/fixtures/files/keywords.csv new file mode 100644 index 0000000..0e6a9ef --- /dev/null +++ b/spec/fixtures/files/keywords.csv @@ -0,0 +1,3 @@ +first keyword +second keyword +third keyword From b974df7d8436fc5c30fc04c690defd3113ef14ce Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 11:25:08 +0700 Subject: [PATCH 32/54] Update controller --- app/controllers/search_stats_controller.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index b2745dc..b185967 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -14,8 +14,14 @@ def create keywords = CSV.parse(csv_file_content).flatten - keywords.map do |keyword| - SearchStat.insert({ keyword: keyword }) + if keywords.any? + keywords.map do |keyword| + SearchStat.insert({ keyword: keyword }) + end + + redirect_to search_stats_path + else + flash[:alert] = "You have selected file with invalid data" end end end From fbd3d54405cdf5e32f1495f5958c8a24204c627a Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 11:42:36 +0700 Subject: [PATCH 33/54] Add tests --- spec/requests/.keep | 0 spec/requests/search_stats_spec.rb | 35 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) delete mode 100644 spec/requests/.keep create mode 100644 spec/requests/search_stats_spec.rb diff --git a/spec/requests/.keep b/spec/requests/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb new file mode 100644 index 0000000..65f1f0b --- /dev/null +++ b/spec/requests/search_stats_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Search Stats', type: :request do + describe 'POST #create' do + context 'given a valid file' do + it 'inserts keywords to the database' do + user = Fabricate(:user) + login_as_user user + + params = { csv_file: fixture_file_upload('keywords.csv', 'text/csv') } + + post :create, params: params + + expect(SearchStat.count).to eq(3) + + expect(page).to redirect_to search_stats_path + end + end + + context 'given an invalid file' do + it 'does not insert keywords to the database' do + user = Fabricate(:user) + login_as_user user + + params = { csv_file: fixture_file_upload('invalid.csv', 'text/csv') } + + post :create, params: params + + expect(SearchStat.count).to eq(0) + end + end + end +end From f3a5ea0eb098020fe051cc244b2337639202a5d8 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 15:25:01 +0700 Subject: [PATCH 34/54] Accept only CSV --- app/views/search_stats/new.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/search_stats/new.html.erb b/app/views/search_stats/new.html.erb index a8b697c..c2ca185 100644 --- a/app/views/search_stats/new.html.erb +++ b/app/views/search_stats/new.html.erb @@ -1,4 +1,4 @@ -<%= form_with url: "/search_stats", multipart: true do |form| %> - <%= file_field_tag :csv_file, class: "form-control" %> - <%= form.submit "Upload", class: "btn btn-primary" %> +<%= form_with url: '/search_stats', multipart: true do |form| %> + <%= file_field_tag :csv_file, accept: 'text/csv', class: 'form-control' %> + <%= form.submit 'Upload', class: 'btn btn-primary' %> <% end %> From d51d97700c12f67921bfbaa618c58088f4260f15 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 16:28:32 +0700 Subject: [PATCH 35/54] Add file validations --- app/controllers/search_stats_controller.rb | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index b185967..90c3f0e 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -3,6 +3,9 @@ require 'csv' class SearchStatsController < ApplicationController + ALLOWED_MIME_TYPE = 'text/csv' + MAXIMUM_FILE_SIZE_BYTES = 1000000 + def new search_stat = SearchStat.new @@ -10,18 +13,20 @@ def new end def create - csv_file_content = params[:csv_file].read + csv_file = params[:csv_file] + + raise 'Invalid file type' unless csv_file.content_type == ALLOWED_MIME_TYPE + raise 'File is too large' unless csv_file.size <= MAXIMUM_FILE_SIZE_BYTES + csv_file_content = params[:csv_file].read keywords = CSV.parse(csv_file_content).flatten - if keywords.any? - keywords.map do |keyword| - SearchStat.insert({ keyword: keyword }) - end - - redirect_to search_stats_path - else - flash[:alert] = "You have selected file with invalid data" + raise 'Invalid file data' unless keywords.any? + + keywords.map do |keyword| + SearchStat.create({ keyword: keyword }) end + + redirect_to root_path end end From f7349baf5473105c169ac612513381791c1db4da Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 16:40:47 +0700 Subject: [PATCH 36/54] Update fixture files --- spec/fixtures/files/empty.csv | 0 spec/fixtures/files/{invalid.csv => invalid_data.csv} | 0 spec/fixtures/files/invalid_type.txt | 3 +++ 3 files changed, 3 insertions(+) delete mode 100644 spec/fixtures/files/empty.csv rename spec/fixtures/files/{invalid.csv => invalid_data.csv} (100%) create mode 100644 spec/fixtures/files/invalid_type.txt diff --git a/spec/fixtures/files/empty.csv b/spec/fixtures/files/empty.csv deleted file mode 100644 index e69de29..0000000 diff --git a/spec/fixtures/files/invalid.csv b/spec/fixtures/files/invalid_data.csv similarity index 100% rename from spec/fixtures/files/invalid.csv rename to spec/fixtures/files/invalid_data.csv diff --git a/spec/fixtures/files/invalid_type.txt b/spec/fixtures/files/invalid_type.txt new file mode 100644 index 0000000..0e6a9ef --- /dev/null +++ b/spec/fixtures/files/invalid_type.txt @@ -0,0 +1,3 @@ +first keyword +second keyword +third keyword From fb1689f6bb3bab9b199acc132409bbfc229462f6 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 16:46:40 +0700 Subject: [PATCH 37/54] Validate maximum keywords --- app/controllers/search_stats_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 90c3f0e..83b9728 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -5,6 +5,7 @@ class SearchStatsController < ApplicationController ALLOWED_MIME_TYPE = 'text/csv' MAXIMUM_FILE_SIZE_BYTES = 1000000 + MAXIMUM_KEYWORDS = 1000 def new search_stat = SearchStat.new @@ -22,6 +23,7 @@ def create keywords = CSV.parse(csv_file_content).flatten raise 'Invalid file data' unless keywords.any? + raise 'Too many keywords' unless keywords.count <= MAXIMUM_KEYWORDS keywords.map do |keyword| SearchStat.create({ keyword: keyword }) From 035589f85f1fb5ab5406ae6e031c4887e4b64d7a Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 17:57:26 +0700 Subject: [PATCH 38/54] Update fixture files --- spec/fixtures/files/keywords.csv | 6 +- spec/fixtures/files/too_many_keywords.csv | 1001 +++++++++++++++++++++ 2 files changed, 1004 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/files/too_many_keywords.csv diff --git a/spec/fixtures/files/keywords.csv b/spec/fixtures/files/keywords.csv index 0e6a9ef..4715482 100644 --- a/spec/fixtures/files/keywords.csv +++ b/spec/fixtures/files/keywords.csv @@ -1,3 +1,3 @@ -first keyword -second keyword -third keyword +1st keyword +2nd keyword +3rd keyword diff --git a/spec/fixtures/files/too_many_keywords.csv b/spec/fixtures/files/too_many_keywords.csv new file mode 100644 index 0000000..cf1496d --- /dev/null +++ b/spec/fixtures/files/too_many_keywords.csv @@ -0,0 +1,1001 @@ +1st keyword +2nd keyword +3rd keyword +4th keyword +5th keyword +6th keyword +7th keyword +8th keyword +9th keyword +10th keyword +11th keyword +12th keyword +13th keyword +14th keyword +15th keyword +16th keyword +17th keyword +18th keyword +19th keyword +20th keyword +21st keyword +22nd keyword +23rd keyword +24th keyword +25th keyword +26th keyword +27th keyword +28th keyword +29th keyword +30th keyword +31st keyword +32nd keyword +33rd keyword +34th keyword +35th keyword +36th keyword +37th keyword +38th keyword +39th keyword +40th keyword +41st keyword +42nd keyword +43rd keyword +44th keyword +45th keyword +46th keyword +47th keyword +48th keyword +49th keyword +50th keyword +51st keyword +52nd keyword +53rd keyword +54th keyword +55th keyword +56th keyword +57th keyword +58th keyword +59th keyword +60th keyword +61st keyword +62nd keyword +63rd keyword +64th keyword +65th keyword +66th keyword +67th keyword +68th keyword +69th keyword +70th keyword +71st keyword +72nd keyword +73rd keyword +74th keyword +75th keyword +76th keyword +77th keyword +78th keyword +79th keyword +80th keyword +81st keyword +82nd keyword +83rd keyword +84th keyword +85th keyword +86th keyword +87th keyword +88th keyword +89th keyword +90th keyword +91st keyword +92nd keyword +93rd keyword +94th keyword +95th keyword +96th keyword +97th keyword +98th keyword +99th keyword +100th keyword +101st keyword +102nd keyword +103rd keyword +104th keyword +105th keyword +106th keyword +107th keyword +108th keyword +109th keyword +110th keyword +111th keyword +112th keyword +113th keyword +114th keyword +115th keyword +116th keyword +117th keyword +118th keyword +119th keyword +120th keyword +121st keyword +122nd keyword +123rd keyword +124th keyword +125th keyword +126th keyword +127th keyword +128th keyword +129th keyword +130th keyword +131st keyword +132nd keyword +133rd keyword +134th keyword +135th keyword +136th keyword +137th keyword +138th keyword +139th keyword +140th keyword +141st keyword +142nd keyword +143rd keyword +144th keyword +145th keyword +146th keyword +147th keyword +148th keyword +149th keyword +150th keyword +151st keyword +152nd keyword +153rd keyword +154th keyword +155th keyword +156th keyword +157th keyword +158th keyword +159th keyword +160th keyword +161st keyword +162nd keyword +163rd keyword +164th keyword +165th keyword +166th keyword +167th keyword +168th keyword +169th keyword +170th keyword +171st keyword +172nd keyword +173rd keyword +174th keyword +175th keyword +176th keyword +177th keyword +178th keyword +179th keyword +180th keyword +181st keyword +182nd keyword +183rd keyword +184th keyword +185th keyword +186th keyword +187th keyword +188th keyword +189th keyword +190th keyword +191st keyword +192nd keyword +193rd keyword +194th keyword +195th keyword +196th keyword +197th keyword +198th keyword +199th keyword +200th keyword +201st keyword +202nd keyword +203rd keyword +204th keyword +205th keyword +206th keyword +207th keyword +208th keyword +209th keyword +210th keyword +211th keyword +212th keyword +213th keyword +214th keyword +215th keyword +216th keyword +217th keyword +218th keyword +219th keyword +220th keyword +221st keyword +222nd keyword +223rd keyword +224th keyword +225th keyword +226th keyword +227th keyword +228th keyword +229th keyword +230th keyword +231st keyword +232nd keyword +233rd keyword +234th keyword +235th keyword +236th keyword +237th keyword +238th keyword +239th keyword +240th keyword +241st keyword +242nd keyword +243rd keyword +244th keyword +245th keyword +246th keyword +247th keyword +248th keyword +249th keyword +250th keyword +251st keyword +252nd keyword +253rd keyword +254th keyword +255th keyword +256th keyword +257th keyword +258th keyword +259th keyword +260th keyword +261st keyword +262nd keyword +263rd keyword +264th keyword +265th keyword +266th keyword +267th keyword +268th keyword +269th keyword +270th keyword +271st keyword +272nd keyword +273rd keyword +274th keyword +275th keyword +276th keyword +277th keyword +278th keyword +279th keyword +280th keyword +281st keyword +282nd keyword +283rd keyword +284th keyword +285th keyword +286th keyword +287th keyword +288th keyword +289th keyword +290th keyword +291st keyword +292nd keyword +293rd keyword +294th keyword +295th keyword +296th keyword +297th keyword +298th keyword +299th keyword +300th keyword +301st keyword +302nd keyword +303rd keyword +304th keyword +305th keyword +306th keyword +307th keyword +308th keyword +309th keyword +310th keyword +311th keyword +312th keyword +313th keyword +314th keyword +315th keyword +316th keyword +317th keyword +318th keyword +319th keyword +320th keyword +321st keyword +322nd keyword +323rd keyword +324th keyword +325th keyword +326th keyword +327th keyword +328th keyword +329th keyword +330th keyword +331st keyword +332nd keyword +333rd keyword +334th keyword +335th keyword +336th keyword +337th keyword +338th keyword +339th keyword +340th keyword +341st keyword +342nd keyword +343rd keyword +344th keyword +345th keyword +346th keyword +347th keyword +348th keyword +349th keyword +350th keyword +351st keyword +352nd keyword +353rd keyword +354th keyword +355th keyword +356th keyword +357th keyword +358th keyword +359th keyword +360th keyword +361st keyword +362nd keyword +363rd keyword +364th keyword +365th keyword +366th keyword +367th keyword +368th keyword +369th keyword +370th keyword +371st keyword +372nd keyword +373rd keyword +374th keyword +375th keyword +376th keyword +377th keyword +378th keyword +379th keyword +380th keyword +381st keyword +382nd keyword +383rd keyword +384th keyword +385th keyword +386th keyword +387th keyword +388th keyword +389th keyword +390th keyword +391st keyword +392nd keyword +393rd keyword +394th keyword +395th keyword +396th keyword +397th keyword +398th keyword +399th keyword +400th keyword +401st keyword +402nd keyword +403rd keyword +404th keyword +405th keyword +406th keyword +407th keyword +408th keyword +409th keyword +410th keyword +411th keyword +412th keyword +413th keyword +414th keyword +415th keyword +416th keyword +417th keyword +418th keyword +419th keyword +420th keyword +421st keyword +422nd keyword +423rd keyword +424th keyword +425th keyword +426th keyword +427th keyword +428th keyword +429th keyword +430th keyword +431st keyword +432nd keyword +433rd keyword +434th keyword +435th keyword +436th keyword +437th keyword +438th keyword +439th keyword +440th keyword +441st keyword +442nd keyword +443rd keyword +444th keyword +445th keyword +446th keyword +447th keyword +448th keyword +449th keyword +450th keyword +451st keyword +452nd keyword +453rd keyword +454th keyword +455th keyword +456th keyword +457th keyword +458th keyword +459th keyword +460th keyword +461st keyword +462nd keyword +463rd keyword +464th keyword +465th keyword +466th keyword +467th keyword +468th keyword +469th keyword +470th keyword +471st keyword +472nd keyword +473rd keyword +474th keyword +475th keyword +476th keyword +477th keyword +478th keyword +479th keyword +480th keyword +481st keyword +482nd keyword +483rd keyword +484th keyword +485th keyword +486th keyword +487th keyword +488th keyword +489th keyword +490th keyword +491st keyword +492nd keyword +493rd keyword +494th keyword +495th keyword +496th keyword +497th keyword +498th keyword +499th keyword +500th keyword +501st keyword +502nd keyword +503rd keyword +504th keyword +505th keyword +506th keyword +507th keyword +508th keyword +509th keyword +510th keyword +511th keyword +512th keyword +513th keyword +514th keyword +515th keyword +516th keyword +517th keyword +518th keyword +519th keyword +520th keyword +521st keyword +522nd keyword +523rd keyword +524th keyword +525th keyword +526th keyword +527th keyword +528th keyword +529th keyword +530th keyword +531st keyword +532nd keyword +533rd keyword +534th keyword +535th keyword +536th keyword +537th keyword +538th keyword +539th keyword +540th keyword +541st keyword +542nd keyword +543rd keyword +544th keyword +545th keyword +546th keyword +547th keyword +548th keyword +549th keyword +550th keyword +551st keyword +552nd keyword +553rd keyword +554th keyword +555th keyword +556th keyword +557th keyword +558th keyword +559th keyword +560th keyword +561st keyword +562nd keyword +563rd keyword +564th keyword +565th keyword +566th keyword +567th keyword +568th keyword +569th keyword +570th keyword +571st keyword +572nd keyword +573rd keyword +574th keyword +575th keyword +576th keyword +577th keyword +578th keyword +579th keyword +580th keyword +581st keyword +582nd keyword +583rd keyword +584th keyword +585th keyword +586th keyword +587th keyword +588th keyword +589th keyword +590th keyword +591st keyword +592nd keyword +593rd keyword +594th keyword +595th keyword +596th keyword +597th keyword +598th keyword +599th keyword +600th keyword +601st keyword +602nd keyword +603rd keyword +604th keyword +605th keyword +606th keyword +607th keyword +608th keyword +609th keyword +610th keyword +611th keyword +612th keyword +613th keyword +614th keyword +615th keyword +616th keyword +617th keyword +618th keyword +619th keyword +620th keyword +621st keyword +622nd keyword +623rd keyword +624th keyword +625th keyword +626th keyword +627th keyword +628th keyword +629th keyword +630th keyword +631st keyword +632nd keyword +633rd keyword +634th keyword +635th keyword +636th keyword +637th keyword +638th keyword +639th keyword +640th keyword +641st keyword +642nd keyword +643rd keyword +644th keyword +645th keyword +646th keyword +647th keyword +648th keyword +649th keyword +650th keyword +651st keyword +652nd keyword +653rd keyword +654th keyword +655th keyword +656th keyword +657th keyword +658th keyword +659th keyword +660th keyword +661st keyword +662nd keyword +663rd keyword +664th keyword +665th keyword +666th keyword +667th keyword +668th keyword +669th keyword +670th keyword +671st keyword +672nd keyword +673rd keyword +674th keyword +675th keyword +676th keyword +677th keyword +678th keyword +679th keyword +680th keyword +681st keyword +682nd keyword +683rd keyword +684th keyword +685th keyword +686th keyword +687th keyword +688th keyword +689th keyword +690th keyword +691st keyword +692nd keyword +693rd keyword +694th keyword +695th keyword +696th keyword +697th keyword +698th keyword +699th keyword +700th keyword +701st keyword +702nd keyword +703rd keyword +704th keyword +705th keyword +706th keyword +707th keyword +708th keyword +709th keyword +710th keyword +711th keyword +712th keyword +713th keyword +714th keyword +715th keyword +716th keyword +717th keyword +718th keyword +719th keyword +720th keyword +721st keyword +722nd keyword +723rd keyword +724th keyword +725th keyword +726th keyword +727th keyword +728th keyword +729th keyword +730th keyword +731st keyword +732nd keyword +733rd keyword +734th keyword +735th keyword +736th keyword +737th keyword +738th keyword +739th keyword +740th keyword +741st keyword +742nd keyword +743rd keyword +744th keyword +745th keyword +746th keyword +747th keyword +748th keyword +749th keyword +750th keyword +751st keyword +752nd keyword +753rd keyword +754th keyword +755th keyword +756th keyword +757th keyword +758th keyword +759th keyword +760th keyword +761st keyword +762nd keyword +763rd keyword +764th keyword +765th keyword +766th keyword +767th keyword +768th keyword +769th keyword +770th keyword +771st keyword +772nd keyword +773rd keyword +774th keyword +775th keyword +776th keyword +777th keyword +778th keyword +779th keyword +780th keyword +781st keyword +782nd keyword +783rd keyword +784th keyword +785th keyword +786th keyword +787th keyword +788th keyword +789th keyword +790th keyword +791st keyword +792nd keyword +793rd keyword +794th keyword +795th keyword +796th keyword +797th keyword +798th keyword +799th keyword +800th keyword +801st keyword +802nd keyword +803rd keyword +804th keyword +805th keyword +806th keyword +807th keyword +808th keyword +809th keyword +810th keyword +811th keyword +812th keyword +813th keyword +814th keyword +815th keyword +816th keyword +817th keyword +818th keyword +819th keyword +820th keyword +821st keyword +822nd keyword +823rd keyword +824th keyword +825th keyword +826th keyword +827th keyword +828th keyword +829th keyword +830th keyword +831st keyword +832nd keyword +833rd keyword +834th keyword +835th keyword +836th keyword +837th keyword +838th keyword +839th keyword +840th keyword +841st keyword +842nd keyword +843rd keyword +844th keyword +845th keyword +846th keyword +847th keyword +848th keyword +849th keyword +850th keyword +851st keyword +852nd keyword +853rd keyword +854th keyword +855th keyword +856th keyword +857th keyword +858th keyword +859th keyword +860th keyword +861st keyword +862nd keyword +863rd keyword +864th keyword +865th keyword +866th keyword +867th keyword +868th keyword +869th keyword +870th keyword +871st keyword +872nd keyword +873rd keyword +874th keyword +875th keyword +876th keyword +877th keyword +878th keyword +879th keyword +880th keyword +881st keyword +882nd keyword +883rd keyword +884th keyword +885th keyword +886th keyword +887th keyword +888th keyword +889th keyword +890th keyword +891st keyword +892nd keyword +893rd keyword +894th keyword +895th keyword +896th keyword +897th keyword +898th keyword +899th keyword +900th keyword +901st keyword +902nd keyword +903rd keyword +904th keyword +905th keyword +906th keyword +907th keyword +908th keyword +909th keyword +910th keyword +911th keyword +912th keyword +913th keyword +914th keyword +915th keyword +916th keyword +917th keyword +918th keyword +919th keyword +920th keyword +921st keyword +922nd keyword +923rd keyword +924th keyword +925th keyword +926th keyword +927th keyword +928th keyword +929th keyword +930th keyword +931st keyword +932nd keyword +933rd keyword +934th keyword +935th keyword +936th keyword +937th keyword +938th keyword +939th keyword +940th keyword +941st keyword +942nd keyword +943rd keyword +944th keyword +945th keyword +946th keyword +947th keyword +948th keyword +949th keyword +950th keyword +951st keyword +952nd keyword +953rd keyword +954th keyword +955th keyword +956th keyword +957th keyword +958th keyword +959th keyword +960th keyword +961st keyword +962nd keyword +963rd keyword +964th keyword +965th keyword +966th keyword +967th keyword +968th keyword +969th keyword +970th keyword +971st keyword +972nd keyword +973rd keyword +974th keyword +975th keyword +976th keyword +977th keyword +978th keyword +979th keyword +980th keyword +981st keyword +982nd keyword +983rd keyword +984th keyword +985th keyword +986th keyword +987th keyword +988th keyword +989th keyword +990th keyword +991st keyword +992nd keyword +993rd keyword +994th keyword +995th keyword +996th keyword +997th keyword +998th keyword +999th keyword +1000th keyword +1001st keyword From 6ec2e4c63f48322433ec0bb86e1d55d718c7c35e Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 18:13:21 +0700 Subject: [PATCH 39/54] Update tests --- spec/requests/search_stats_spec.rb | 40 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index 65f1f0b..b9fa2ce 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Search Stats', type: :request do describe 'POST #create' do context 'given a valid file' do - it 'inserts keywords to the database' do + it 'inserts keywords to the database and redirects to search stat index' do user = Fabricate(:user) login_as_user user @@ -19,16 +19,48 @@ end end - context 'given an invalid file' do - it 'does not insert keywords to the database' do + context 'given an invalid file data' do + it 'does not insert keywords to the database and raises an error' do user = Fabricate(:user) login_as_user user - params = { csv_file: fixture_file_upload('invalid.csv', 'text/csv') } + params = { csv_file: fixture_file_upload('invalid_data.csv', 'text/csv') } post :create, params: params expect(SearchStat.count).to eq(0) + + expect(page).to raise_error(RuntimeError, 'Invalid file data') + end + end + + context 'given an invalid file type' do + it 'does not insert keywords to the database and raises an error' do + user = Fabricate(:user) + login_as_user user + + params = { csv_file: fixture_file_upload('invalid_type.txt', 'text/plain') } + + post :create, params: params + + expect(SearchStat.count).to eq(0) + + expect(page).to raise_error(RuntimeError, 'Invalid file type') + end + end + + context 'given too many keywords' do + it 'does not insert keywords to the database and raises an error' do + user = Fabricate(:user) + login_as_user user + + params = { csv_file: fixture_file_upload('invalid_type.txt', 'text/plain') } + + post :create, params: params + + expect(SearchStat.count).to eq(0) + + expect(page).to raise_error(RuntimeError, 'Too many keywords') end end end From 3edef7e9926613d316431342cd8c06217dac899e Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 18:14:56 +0700 Subject: [PATCH 40/54] Add support file --- spec/support/devise.rb | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 spec/support/devise.rb diff --git a/spec/support/devise.rb b/spec/support/devise.rb new file mode 100644 index 0000000..f2d43a6 --- /dev/null +++ b/spec/support/devise.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include Devise::Test::ControllerHelpers, type: :view + config.include Devise::Test::ControllerHelpers, type: :helper + config.include Devise::Test::IntegrationHelpers, type: :feature + config.include Devise::Test::IntegrationHelpers, type: :request +end From 2004806ed38799cd54f1bd5f539e9f7b25e09ee0 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 18:23:19 +0700 Subject: [PATCH 41/54] Remove manual file size check --- app/controllers/search_stats_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 83b9728..79b2703 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -4,7 +4,6 @@ class SearchStatsController < ApplicationController ALLOWED_MIME_TYPE = 'text/csv' - MAXIMUM_FILE_SIZE_BYTES = 1000000 MAXIMUM_KEYWORDS = 1000 def new @@ -17,7 +16,6 @@ def create csv_file = params[:csv_file] raise 'Invalid file type' unless csv_file.content_type == ALLOWED_MIME_TYPE - raise 'File is too large' unless csv_file.size <= MAXIMUM_FILE_SIZE_BYTES csv_file_content = params[:csv_file].read keywords = CSV.parse(csv_file_content).flatten From 56b8585904780ede5b8c47aca55547d567bb7f5a Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 18:47:12 +0700 Subject: [PATCH 42/54] Update tests --- spec/requests/search_stats_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index b9fa2ce..e87f9f2 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -54,7 +54,7 @@ user = Fabricate(:user) login_as_user user - params = { csv_file: fixture_file_upload('invalid_type.txt', 'text/plain') } + params = { csv_file: fixture_file_upload('too_many_keywords.csv', 'text/plain') } post :create, params: params From db940aaf92c056bd8838a061c15c7638ac34e728 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Wed, 31 May 2023 19:52:11 +0700 Subject: [PATCH 43/54] Update path --- spec/requests/search_stats_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index e87f9f2..c3ab3ef 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -11,7 +11,7 @@ params = { csv_file: fixture_file_upload('keywords.csv', 'text/csv') } - post :create, params: params + post search_stats_path, params: params expect(SearchStat.count).to eq(3) @@ -26,7 +26,7 @@ params = { csv_file: fixture_file_upload('invalid_data.csv', 'text/csv') } - post :create, params: params + post search_stats_path, params: params expect(SearchStat.count).to eq(0) @@ -41,7 +41,7 @@ params = { csv_file: fixture_file_upload('invalid_type.txt', 'text/plain') } - post :create, params: params + post search_stats_path, params: params expect(SearchStat.count).to eq(0) @@ -56,7 +56,7 @@ params = { csv_file: fixture_file_upload('too_many_keywords.csv', 'text/plain') } - post :create, params: params + post search_stats_path, params: params expect(SearchStat.count).to eq(0) From 9fb4021490573849f103f68d1e0756924e89b28f Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:36:26 +0700 Subject: [PATCH 44/54] Update fixture files --- spec/fixtures/files/invalid_type.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/fixtures/files/invalid_type.txt b/spec/fixtures/files/invalid_type.txt index 0e6a9ef..4715482 100644 --- a/spec/fixtures/files/invalid_type.txt +++ b/spec/fixtures/files/invalid_type.txt @@ -1,3 +1,3 @@ -first keyword -second keyword -third keyword +1st keyword +2nd keyword +3rd keyword From 526182bedc46ee6383223acb04aad2ff1e3a77ac Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:39:31 +0700 Subject: [PATCH 45/54] Update redirection --- app/controllers/search_stats_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 79b2703..c464e19 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -27,6 +27,6 @@ def create SearchStat.create({ keyword: keyword }) end - redirect_to root_path + redirect_to search_stats_path end end From 8241db720bd77a1b61939d6a7ed97806fc558b9d Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 15:41:49 +0700 Subject: [PATCH 46/54] Fix tests --- spec/requests/search_stats_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index c3ab3ef..dd288cf 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -54,7 +54,7 @@ user = Fabricate(:user) login_as_user user - params = { csv_file: fixture_file_upload('too_many_keywords.csv', 'text/plain') } + params = { csv_file: fixture_file_upload('too_many_keywords.csv', 'text/csv') } post search_stats_path, params: params From 565303c66b8915e6e9d5aea9892a7081b8e9a60d Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:56:39 +0700 Subject: [PATCH 47/54] Update tests --- spec/requests/search_stats_spec.rb | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index dd288cf..27d63be 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -26,11 +26,9 @@ params = { csv_file: fixture_file_upload('invalid_data.csv', 'text/csv') } - post search_stats_path, params: params - - expect(SearchStat.count).to eq(0) + expect { post search_stats_path, params: params }.to raise_error(RuntimeError, 'Invalid file data') - expect(page).to raise_error(RuntimeError, 'Invalid file data') + expect(SearchStat.all.count).to eq(0) end end @@ -41,11 +39,9 @@ params = { csv_file: fixture_file_upload('invalid_type.txt', 'text/plain') } - post search_stats_path, params: params - - expect(SearchStat.count).to eq(0) + expect { post search_stats_path, params: params }.to raise_error(RuntimeError, 'Invalid file type') - expect(page).to raise_error(RuntimeError, 'Invalid file type') + expect(SearchStat.all.count).to eq(0) end end @@ -56,11 +52,9 @@ params = { csv_file: fixture_file_upload('too_many_keywords.csv', 'text/csv') } - post search_stats_path, params: params - - expect(SearchStat.count).to eq(0) + expect { post search_stats_path, params: params }.to raise_error(RuntimeError, 'Too many keywords') - expect(page).to raise_error(RuntimeError, 'Too many keywords') + expect(SearchStat.all.count).to eq(0) end end end From 2ce0def780550be615bec766ca2b8f6ded183804 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:02:33 +0700 Subject: [PATCH 48/54] Update code --- app/controllers/search_stats_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 69341f4..669ee25 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -23,7 +23,7 @@ def create raise 'Invalid file type' unless csv_file.content_type == ALLOWED_MIME_TYPE csv_file_content = params[:csv_file].read - keywords = CSV.parse(csv_file_content).flatten + keywords = CSV.parse(csv_file_content).flatten.compact_blank raise 'Invalid file data' unless keywords.any? raise 'Too many keywords' unless keywords.count <= MAXIMUM_KEYWORDS From 4925fd227b5645cd883f0d7ec091801c2da7d104 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:08:50 +0700 Subject: [PATCH 49/54] Update tests --- spec/requests/search_stats_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/search_stats_spec.rb b/spec/requests/search_stats_spec.rb index 27d63be..cc015a9 100644 --- a/spec/requests/search_stats_spec.rb +++ b/spec/requests/search_stats_spec.rb @@ -13,9 +13,9 @@ post search_stats_path, params: params - expect(SearchStat.count).to eq(3) + expect(response).to redirect_to search_stats_path - expect(page).to redirect_to search_stats_path + expect(SearchStat.all.count).to eq(3) end end From 93d154826e2d5dbd40e0b2ad5af1035c15c6ed30 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:24:59 +0700 Subject: [PATCH 50/54] Update code --- app/controllers/search_stats_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 669ee25..18b99c4 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -29,7 +29,8 @@ def create raise 'Too many keywords' unless keywords.count <= MAXIMUM_KEYWORDS keywords.map do |keyword| - SearchStat.create({ keyword: keyword }) + search_stat = SearchStat.new({ keyword: keyword }) + search_stat.save end redirect_to search_stats_path From 9c29c043492ab891c19e830413875cc687a41364 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:46:10 +0700 Subject: [PATCH 51/54] Update seeds --- db/seeds.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/seeds.rb b/db/seeds.rb index 4ca4fbb..648b5f0 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -8,6 +8,8 @@ require 'fabrication' +Fabricate(:user, email: 'demo@example.com', password: 'nimbleHq1234') + # Generate dummy data for SearchStat 10.times do Fabricate(:search_stat) From 4895f2e4552feca76ab96d22d93513b282065f46 Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:39:57 +0700 Subject: [PATCH 52/54] Insert current user ID --- app/controllers/search_stats_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/search_stats_controller.rb b/app/controllers/search_stats_controller.rb index 18b99c4..1376cb7 100644 --- a/app/controllers/search_stats_controller.rb +++ b/app/controllers/search_stats_controller.rb @@ -29,8 +29,8 @@ def create raise 'Too many keywords' unless keywords.count <= MAXIMUM_KEYWORDS keywords.map do |keyword| - search_stat = SearchStat.new({ keyword: keyword }) - search_stat.save + search_stat = SearchStat.new({ keyword: keyword, user_id: current_user.id }) + search_stat.save! end redirect_to search_stats_path From 215cc21a1a84321e8838211b0657e265edcca01c Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:40:31 +0700 Subject: [PATCH 53/54] Remove validation --- app/models/search_stat.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/search_stat.rb b/app/models/search_stat.rb index 82c3194..3aeb13f 100644 --- a/app/models/search_stat.rb +++ b/app/models/search_stat.rb @@ -2,5 +2,4 @@ class SearchStat < ApplicationRecord validates :keyword, presence: true - validates :raw_response, presence: true end From 91e5b47f2d477b0252ccef5cff3c574d8bc65caa Mon Sep 17 00:00:00 2001 From: topnimble <90754900+topnimble@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:48:41 +0700 Subject: [PATCH 54/54] Update style --- app/views/search_stats/new.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/search_stats/new.html.erb b/app/views/search_stats/new.html.erb index c2ca185..c131f00 100644 --- a/app/views/search_stats/new.html.erb +++ b/app/views/search_stats/new.html.erb @@ -1,4 +1,4 @@ <%= form_with url: '/search_stats', multipart: true do |form| %> <%= file_field_tag :csv_file, accept: 'text/csv', class: 'form-control' %> - <%= form.submit 'Upload', class: 'btn btn-primary' %> + <%= form.submit 'Upload', class: 'btn btn-primary mt-3' %> <% end %>