diff --git a/.rspec b/.rspec index 5be63fc..c99d2e7 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1 @@ --require spec_helper ---format documentation diff --git a/Gemfile b/Gemfile index 44e25a6..37e1ddd 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,8 @@ gem "puma", "~> 4.1" gem "rack-cors" gem "rails", "~> 6.0.2", ">= 6.0.2.2" gem "redis", "~> 4.0" +gem "active_model_serializers" +gem "kaminari" group :development, :test do gem "byebug", platforms: %i[mri mingw x64_mingw] @@ -28,4 +30,4 @@ group :development do gem "spring-watcher-listen", "~> 2.0.0" end -gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 4ac7e5a..2a6f4da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,6 +37,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.10) + actionpack (>= 4.1, < 6.1) + activemodel (>= 4.1, < 6.1) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) activejob (6.0.2.2) activesupport (= 6.0.2.2) globalid (>= 0.3.6) @@ -61,6 +66,8 @@ GEM msgpack (~> 1.0) builder (3.2.4) byebug (11.1.1) + case_transform (0.2) + activesupport coderay (1.1.2) concurrent-ruby (1.1.6) crass (1.0.6) @@ -79,6 +86,19 @@ GEM i18n (1.8.2) concurrent-ruby (~> 1.0) jaro_winkler (1.5.4) + jsonapi-renderer (0.2.2) + kaminari (1.2.0) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.0) + kaminari-activerecord (= 1.2.0) + kaminari-core (= 1.2.0) + kaminari-actionview (1.2.0) + actionview + kaminari-core (= 1.2.0) + kaminari-activerecord (1.2.0) + activerecord + kaminari-core (= 1.2.0) + kaminari-core (1.2.0) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -100,7 +120,7 @@ GEM nokogiri (1.10.9) mini_portile2 (~> 2.4.0) parallel (1.19.1) - parser (2.7.0.5) + parser (2.7.1.0) ast (~> 2.4.0) pg (1.2.3) pry (0.13.0) @@ -163,14 +183,14 @@ GEM rspec-mocks (~> 3.9) rspec-support (~> 3.9) rspec-support (3.9.2) - rubocop (0.80.1) + rubocop (0.81.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) rexml ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.7) + unicode-display_width (>= 1.4.0, < 2.0) ruby-progressbar (1.10.1) ruby_dep (1.5.0) shoulda-matchers (4.3.0) @@ -188,9 +208,9 @@ GEM sprockets (>= 3.0.0) thor (1.0.1) thread_safe (0.3.6) - tzinfo (1.2.6) + tzinfo (1.2.7) thread_safe (~> 0.1) - unicode-display_width (1.6.1) + unicode-display_width (1.7.0) websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.4) @@ -200,10 +220,12 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers bootsnap (>= 1.4.2) byebug factory_bot_rails faker + kaminari listen (>= 3.0.5, < 3.2) pg (>= 0.18, < 2.0) pry @@ -222,4 +244,4 @@ RUBY VERSION ruby 2.7.0p0 BUNDLED WITH - 2.1.4 + 2.1.2 diff --git a/README.md b/README.md index 7db80e4..54b3185 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,87 @@ -# README +# Search for opportunities job for technology professionals +This project is a search engine for job opportunities for technology professionals. -This README would normally document whatever steps are necessary to get the -application up and running. +# Stack +* docker +* API: Ruby On Rails +* web client: React ou VueJs +* Chatbot: DialogFlow -Things you may want to cover: +*Search for opportunities job* -* Ruby version +> You can try here: *insert app url( [insert app url] )*. -* System dependencies +# Docker Container -* Configuration +This project was done using docker container, the container name is Nameprogramadorhomeofficeapi_app -* Database creation +## Getting Started -* Database initialization +These instructions will cover usage information and for the docker container -* How to run the test suite +### Prerequisities -* Services (job queues, cache servers, search engines, etc.) +In order to run this container you'll need docker installed. -* Deployment instructions +* [Windows](https://docs.docker.com/windows/started) +* [OS X](https://docs.docker.com/mac/started/) +* [Linux](https://docs.docker.com/linux/started/) -* ... +## Installation + +clone: +``` +$ git clone git@github.com:OneBitCodeBlog/programador-homeoffice-api.git +$ cd programador-homeoffice-api +``` + +gems install: +``` +$ docker-compose run --rm app bundle install +``` +RSpec install: +``` +docker-compose run --rm app bundle exec rails generate rspec:install +``` +generate database: +``` +$ docker-compose run --rm app bundle exec rails db:create db:migrate + +``` +run the app: +``` +$ docker-compose up --build +``` +Access: +* [opportunities job here](http://0.0.0.0:3000/) + +## PS. if an error like this happens + * Listening on tcp://0.0.0.0:3000 +app_1 | bundler: failed to load command: puma (/gems/ruby/2.7.0/bin/puma) +app_1 | Errno::ENOENT: No such file or directory @ rb_sysopen - tmp/pids/server.pid +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/launcher.rb:216:in `initialize' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/launcher.rb:216:in `open' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/launcher.rb:216:in `write_pid' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/launcher.rb:105:in `write_state' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/single.rb:103:in `run' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/launcher.rb:172:in `run' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/lib/puma/cli.rb:80:in `run' +app_1 | /gems/ruby/2.7.0/gems/puma-4.3.3/bin/puma:10:in `' +app_1 | /gems/ruby/2.7.0/bin/puma:23:in `load' +app_1 | /gems/ruby/2.7.0/bin/puma:23:in `' +programadorhomeofficeapi_app_1 exited with code 1 + +### you can try this solution: +in a shell prompt you must create tmp/pips folder in your machine with: +``` +$ mkdir -p tmp/pids/ +``` +now run again: +``` +$ docker-compose up --build +``` +and now, must be working! + +## License + +This project is licensed under the MIT License \ No newline at end of file diff --git a/app/controllers/api/v1/jobs_controller.rb b/app/controllers/api/v1/jobs_controller.rb new file mode 100644 index 0000000..74b5209 --- /dev/null +++ b/app/controllers/api/v1/jobs_controller.rb @@ -0,0 +1,23 @@ +module API + module V1 + class JobsController < ApplicationController + def index + page = params[:page] + page_number = page.try(:[], :number) + per_page = page.try(:[], :size) + + @jobs = Job.all.page(page_number).per(per_page) + + render json: @jobs + end + + def show + @job = Job.find(params[:id]) + + render json: @job + rescue ActiveRecord::RecordNotFound + head :not_found + end + end + end +end diff --git a/app/controllers/chatbot_controller.rb b/app/controllers/chatbot_controller.rb new file mode 100644 index 0000000..0530fef --- /dev/null +++ b/app/controllers/chatbot_controller.rb @@ -0,0 +1,14 @@ +class ChatbotController < ApplicationController + + def webhook + + request.body.rewind + result = JSON.parse(request.body.read) + action = result["queryResult"]["action"] + + response = InterpretService.call(action: action, result: result) + + render :json => response + + end +end diff --git a/app/serializers/job_serializer.rb b/app/serializers/job_serializer.rb new file mode 100644 index 0000000..3ef1ca9 --- /dev/null +++ b/app/serializers/job_serializer.rb @@ -0,0 +1,6 @@ +class JobSerializer < ActiveModel::Serializer + attributes :id, :title, :description, :contract, + :job_link, :salary, :company_name, :published_date + + has_many :key_words +end diff --git a/app/services/create_alert_service.rb b/app/services/create_alert_service.rb new file mode 100644 index 0000000..37feba2 --- /dev/null +++ b/app/services/create_alert_service.rb @@ -0,0 +1,32 @@ +class CreateAlertService + + def initialize(session:, language:) + @session = session + @language = language + end + + def call + key_word = KeyWord.find_by(tag: @language) + if key_word.nil? + KeyWord.create!( + tag: @language + ) + key_word = KeyWord.last + end + Search.create!( + user: User.find_by(session_id: @session), + alarm_rate: 1, + key_word: key_word + ) + response = { + "fulfillmentText": "Vagas - Remoto", + "fulfillmentMessages": { + "text": { + "text": [ + "Seu alarme foi criado com sucesso. Assim que uma nova vaga para #{@language} surgir você receberá um alerta" + ] + } + } + } + end +end \ No newline at end of file diff --git a/app/services/create_user_service.rb b/app/services/create_user_service.rb new file mode 100644 index 0000000..3acd1b7 --- /dev/null +++ b/app/services/create_user_service.rb @@ -0,0 +1,14 @@ +class CreateUserService + + def initialize(session:, name:) + @session = session + @name = name + end + + def call + User.create!( + name: @name, + session_id: @session + ) + end +end \ No newline at end of file diff --git a/app/services/delete_alert_service.rb b/app/services/delete_alert_service.rb new file mode 100644 index 0000000..09bd60f --- /dev/null +++ b/app/services/delete_alert_service.rb @@ -0,0 +1,43 @@ +class DeleteAlertService + + def initialize(session:, tag:) + @session = session + if tag == 'todos' + @tag = nil + else + @tag = tag + end + end + + def call + hashes = [] + if @tag.nil? + alerts = Search.where(user: User.find_by(session_id: @session)) + alerts.each do |alert| + alert.destroy + hashes << { + "card": { + "title": "Todos seus alertas foram excluídos com sucesso" + } + } + end + else + key_word = KeyWord.find_by(tag: @tag) + alerts = Search.find_by( + user: User.find_by(session_id: @session), + key_word: key_word + ) + alerts.destroy + hashes << { + "card": { + "title": "Seu alerta da palavra chave #{@alert} foi excluído com sucesso" + } + } + end + + response = { + "fulfillmentText": "Vagas - Remoto", + "fulfillmentMessages": hashes + } + end +end \ No newline at end of file diff --git a/app/services/interpret_service.rb b/app/services/interpret_service.rb new file mode 100644 index 0000000..a19077d --- /dev/null +++ b/app/services/interpret_service.rb @@ -0,0 +1,28 @@ +class InterpretService + + def self.call(action:, result:) + case action + when "search" + keyword = result["queryResult"]["parameters"]["keyword"] + SearchJobService.new(keyword: keyword).call() + when "alert" + session = result["session"].split('/')[-1] + language = result["queryResult"]["parameters"]["linguagem"] + CreateAlertService.new(session: session, language: language).call + when "list" + session = result["session"].split('/')[-1] + ListAlertService.new(session: session).call + when "delete" + session = result["session"].split('/')[-1] + tag = result["queryResult"]["parameters"]["IDAlarm"] + DeleteAlertService.new(session: session, tag: tag).call + when "input.welcome" + session = result["session"].split('/')[-1] + name = result["queryResult"]["parameters"]["name"] + CreateUserService.new(session: session, name: name).call() + else + "Desculpe, mas não te entendi. Tente novamente" + end + end + +end \ No newline at end of file diff --git a/app/services/list_alert_service.rb b/app/services/list_alert_service.rb new file mode 100644 index 0000000..276a8d8 --- /dev/null +++ b/app/services/list_alert_service.rb @@ -0,0 +1,31 @@ +class ListAlertService + + def initialize(session:) + @session = session + end + + def call + alerts = Search.where(user: User.find_by(session_id: @session)) + hashes = [] + if alerts.blank? + hashes << { + "card": { + "title": "Você não tem nenhum alerta cadastrado" + } + } + else + alerts.each do |alert| + hashes << { + "card": { + "title": "Seus alertas", + "subtitle": "Você tem um alerta para a palavra-chave: #{alert.key_word.tag}", + } + } + end + end + response = { + "fulfillmentText": "Vagas - Remoto", + "fulfillmentMessages": hashes + } + end +end \ No newline at end of file diff --git a/app/services/search_job_service.rb b/app/services/search_job_service.rb new file mode 100644 index 0000000..4625c27 --- /dev/null +++ b/app/services/search_job_service.rb @@ -0,0 +1,30 @@ +class SearchJobService + + def initialize(keyword:) + @keyword = keyword + end + + def call + keyword = KeyWord.find_by(tag: @keyword) + jobs = JobKeyWord.where(key_word: keyword) + hashes = [] + jobs.each do |job| + hashes << { + "card": { + "title": job.job.title, + "subtitle": "#{job.job.description} - Publicado em: #{job.job.published_date.strftime("%d/%m/%Y")}", + "buttons": [ + { + "text": "Ler mais", + "postback": job.job.job_link + } + ] + } + } + end + response = { + "fulfillmentText": "Vagas - Remoto", + "fulfillmentMessages": hashes + } + end +end \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index f84afae..136567e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Rails.application.configure do + config.hosts << "019ea77c.ngrok.io" # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on diff --git a/config/initializers/ams.rb b/config/initializers/ams.rb new file mode 100644 index 0000000..8957206 --- /dev/null +++ b/config/initializers/ams.rb @@ -0,0 +1 @@ +ActiveModelSerializers.config.adapter = ActiveModelSerializers::Adapter::JsonApi \ No newline at end of file diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index aa7435f..4af48f7 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -15,3 +15,7 @@ # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end + +ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym "API" +end diff --git a/config/routes.rb b/config/routes.rb index cefb14f..aad3008 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true Rails.application.routes.draw do - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + namespace :api do + namespace :v1 do + resources :jobs, only: %i[index show] + end + end end diff --git a/db/migrate/20200403023247_add_session_id_to_users.rb b/db/migrate/20200403023247_add_session_id_to_users.rb new file mode 100644 index 0000000..def0b3b --- /dev/null +++ b/db/migrate/20200403023247_add_session_id_to_users.rb @@ -0,0 +1,5 @@ +class AddSessionIdToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :session_id, :string + end +end diff --git a/db/migrate/20200405053502_change_default_for_job_contract.rb b/db/migrate/20200405053502_change_default_for_job_contract.rb new file mode 100644 index 0000000..3ee22a3 --- /dev/null +++ b/db/migrate/20200405053502_change_default_for_job_contract.rb @@ -0,0 +1,5 @@ +class ChangeDefaultForJobContract < ActiveRecord::Migration[6.0] + def change + change_column :jobs, :contract, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index bb58b23..f011329 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_03_31_024104) do +ActiveRecord::Schema.define(version: 2020_04_05_053502) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -27,7 +27,7 @@ create_table "jobs", force: :cascade do |t| t.string "title" t.text "description" - t.integer "contract" + t.integer "contract", default: 0 t.string "job_link" t.integer "salary" t.string "company_name" @@ -56,6 +56,7 @@ t.string "name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "session_id" end add_foreign_key "searches", "key_words" diff --git a/db/seeds.rb b/db/seeds.rb index 8744e3c..cd17edf 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,4 +5,32 @@ # Examples: # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) +# # Character.create(name: 'Luke', movie: movies.first) + +Job.create!( + title: 'Desenvolvedor Full Stack Rails', + description: Faker::Lorem.paragraph, + job_link: Faker::Internet.url, + published_date: Faker::Date.between(from: 2.days.ago, to: Date.today) +) + +Job.create!( + title: 'Desenvolvedor Full Stack Ruby on Rails', + description: Faker::Lorem.paragraph, + job_link: Faker::Internet.url, + published_date: Faker::Date.between(from: 2.days.ago, to: Date.today) +) + +KeyWord.create!( + tag: 'Rails' +) + +JobKeyWord.create!( + job: Job.find(1), + key_word: KeyWord.find(1) +) + +JobKeyWord.create!( + job: Job.find(2), + key_word: KeyWord.find(1) +) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index caf85f3..29635ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,4 +31,4 @@ services: volumes: redis: postgres: - gems: + gems: \ No newline at end of file diff --git a/spec/factories/job_key_words.rb b/spec/factories/job_key_words.rb new file mode 100644 index 0000000..d398a8c --- /dev/null +++ b/spec/factories/job_key_words.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :job_key_word do + association :job, strategy: :build + association :key_word, strategy: :build + end +end diff --git a/spec/factories/jobs.rb b/spec/factories/jobs.rb new file mode 100644 index 0000000..7b1802e --- /dev/null +++ b/spec/factories/jobs.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :job do + title { Faker::Lorem.sentence } + description { Faker::Lorem.paragraph } + job_link { Faker::Internet.url } + published_date { Date.today } + end +end diff --git a/spec/factories/key_words.rb b/spec/factories/key_words.rb new file mode 100644 index 0000000..a4e270d --- /dev/null +++ b/spec/factories/key_words.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :key_word do + tag { Faker::Lorem.word } + end +end diff --git a/spec/models/key_word_spec.rb b/spec/models/key_word_spec.rb index f1f8909..cebe751 100644 --- a/spec/models/key_word_spec.rb +++ b/spec/models/key_word_spec.rb @@ -4,11 +4,9 @@ describe "associations" do it { is_expected.to have_many(:job_key_words).dependent(:destroy) } it { is_expected.to have_many(:jobs).through(:job_key_words) } - it { is_expected.to have_many(:searches).dependent(:destroy) } - it { is_expected.to have_many(:users).through(:searches) } end describe "validations" do it { is_expected.to validate_presence_of(:tag) } end -end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 73b032c..cf009b0 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -28,4 +28,4 @@ config.use_transactional_fixtures = true config.infer_spec_type_from_file_location! config.filter_rails_from_backtrace! -end +end \ No newline at end of file diff --git a/spec/requests/api/v1/jobs_request_spec.rb b/spec/requests/api/v1/jobs_request_spec.rb new file mode 100644 index 0000000..7106995 --- /dev/null +++ b/spec/requests/api/v1/jobs_request_spec.rb @@ -0,0 +1,91 @@ +require "rails_helper" + +RSpec.describe "Jobs", type: :request do + let(:job) do + create(:job).tap do |job| + create(:job_key_word, job: job) + end + end + + let(:parsed_response) { JSON.parse(response.body) } + let(:json_attributes) do + %w[ + title + description + contract + job-link + salary + company-name + published-date + ] + end + + describe "GET #index" do + subject(:index) { get api_v1_jobs_url } + + before do + job + index + end + + shared_examples "request index" do + it "responds with 200" do + expect(response).to have_http_status :ok + end + + it "responds with json" do + expect(parsed_response["data"].first["attributes"].keys) + .to match_array json_attributes + end + end + + shared_examples "pagination" do + it "shows links for pagination" do + expect(parsed_response["links"].keys).to match_array json_links + end + end + + let(:job_attributes) { job.attributes } + let(:json_links) { %w[self first prev next last] } + + include_examples "request index" + include_examples "pagination" + + context "with pagination params" do + subject(:index) do + get api_v1_jobs_url, params: { page: { number: 1, size: 1 } } + end + + include_examples "request index" + include_examples "pagination" + end + end + + describe "GET #show" do + subject(:show) { get api_v1_job_url(job_id) } + + before do + job + show + end + + let(:job_id) { job.id } + + it "responds with 200" do + expect(response).to have_http_status :ok + end + + it "responds with json" do + expect(parsed_response["data"]["attributes"].keys) + .to match_array json_attributes + end + + context "without parameter id" do + let(:job_id) { 1234 } + + it "returns 404" do + expect(response).to have_http_status :not_found + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a7d360f..0dd2047 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,5 +9,7 @@ mocks.verify_partial_doubles = true end + config.order = :random + config.shared_context_metadata_behavior = :apply_to_host_groups -end +end \ No newline at end of file