Commit 60268529 by Mai Hoang Thai Ha

Merge branch 'master' into 'Task/13_ID15_ID14_ID16'

Master

See merge request !13
parents dd0f954b a06436f1
...@@ -31,9 +31,9 @@ gem 'bootsnap', '>= 1.4.4', require: false ...@@ -31,9 +31,9 @@ gem 'bootsnap', '>= 1.4.4', require: false
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'pry-rails', '~> 0.3.9'
# gem 'pry-nav', '~> 0.3.0'
gem 'pry', '~> 0.14.1' gem 'pry', '~> 0.14.1'
# gem 'pry-nav'
gem 'pry-rails'
end end
group :development do group :development do
......
...@@ -192,7 +192,7 @@ GEM ...@@ -192,7 +192,7 @@ GEM
rake (>= 0.13) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.3) rake (13.0.6)
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
...@@ -210,7 +210,7 @@ GEM ...@@ -210,7 +210,7 @@ GEM
rubocop-ast (>= 1.7.0, < 2.0) rubocop-ast (>= 1.7.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0) rubocop-ast (1.8.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-rails (2.11.3) rubocop-rails (2.11.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
...@@ -235,7 +235,7 @@ GEM ...@@ -235,7 +235,7 @@ GEM
slim (4.1.0) slim (4.1.0)
temple (>= 0.7.6, < 0.9) temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1) tilt (>= 2.0.6, < 2.1)
slim-rails (3.2.0) slim-rails (3.3.0)
actionpack (>= 3.1) actionpack (>= 3.1)
railties (>= 3.1) railties (>= 3.1)
slim (>= 3.0, < 5.0) slim (>= 3.0, < 5.0)
...@@ -298,7 +298,7 @@ DEPENDENCIES ...@@ -298,7 +298,7 @@ DEPENDENCIES
mysql2 (~> 0.5) mysql2 (~> 0.5)
nokogiri (~> 1.11, >= 1.11.7) nokogiri (~> 1.11, >= 1.11.7)
pry (~> 0.14.1) pry (~> 0.14.1)
pry-rails (~> 0.3.9) pry-rails
puma (~> 5.0) puma (~> 5.0)
rack-mini-profiler (~> 2.0) rack-mini-profiler (~> 2.0)
rails (~> 6.1.3, >= 6.1.3.2) rails (~> 6.1.3, >= 6.1.3.2)
......
// Place all the styles related to the Applies controller here.
// They will automatically be included in application.css. .ribbon {
// You can use Sass (SCSS) here: https://sass-lang.com/ text-align: center;
ul {
list-style: none;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
margin: 0;
padding: 0;
.item {
display: block;
float: left;
width: 30%;
height: 100%;
background: #f3f5fa;
text-align: center;
padding: 8px;
position: relative;
font-size: 16px;
font-weight: 700;
text-decoration: none;
color: #808080;
.circle {
display: inline-flex;
-ms-flex-line-pack: center;
align-content: center;
-ms-flex-pack: center;
justify-content: center;
width: 1.6em;
height: 1.6em;
padding: 0.25em 0;
margin-right: 10px;
line-height: 1em;
font-size: 1em;
color: #808080;
background-color: #e0e0e0;
border-radius: 50%;
}
&:after {
content: "";
border-top: 21px solid transparent;
border-bottom: 21px solid transparent;
border-left: 21px solid #f3f5fa;
position: absolute;
right: -21px;
top: 0;
z-index: 1;
}
&:last-child::after {
display: none;
}
&:first-child::before {
content: "";
border-top: 21px solid transparent;
border-bottom: 21px solid transparent;
border-left: 21px solid #fff;
position: absolute;
left: 0;
top: 0;
}
}
.active {
background: #0d6efd;
color: #fff;
.circle {
color: #0d6efd;
background-color: #fff;
}
&::after {
border-left-color: #0d6efd;
color: #fff;
}
}
}
}
.btn-height {
height: 50px;
}
.invalid {
.form-msg {
color: #f33a58;
}
input {
border-color: #f33a58;
}
}
// Place all the styles related to the Cities controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// @import "bootstrap"; // @import "bootstrap";
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
// reset css // reset css
*{ *{
...@@ -34,12 +23,12 @@ header { ...@@ -34,12 +23,12 @@ header {
li { li {
margin-left: 18px; margin-left: 18px;
a { a {
color: $gray-400; color: #ced4da;
font-size: 18px; font-size: 18px;
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
&:hover{ &:hover{
color: $white; color: #fff;
} }
} }
} }
...@@ -58,9 +47,9 @@ footer { ...@@ -58,9 +47,9 @@ footer {
color: #777; color: #777;
a { a {
text-decoration:none; text-decoration:none;
color: $gray-600; color: #6c757d;
&:hover { &:hover {
color: $gray-900; color: #212529;
} }
} }
small { small {
......
// Place all the styles related to the Industries controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the Jobs controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the Top controller here. // Top page
// They will automatically be included in application.css. // search
// You can use Sass (SCSS) here: https://sass-lang.com/ // banner
.banner {
background-image: url('banner.jpg');
min-height: 200px;
background-repeat: no-repeat;
background-size: cover;
}
// latest-job
.latest-job {
.job-item {
font-size: 14px;
.job-title {
text-decoration: none;
color: black;
font-size: 20px;
font-weight: 500;
}
.job-caption {
line-height: 18px;
.job-company {
text-decoration: none;
font-size: 14px;
color: #495057;
}
.job-salary {
color: #008563;
margin: 0;
}
.job-locations {
ul {
list-style: none;
margin: 0 0 6px 0;
padding: 0;
color: #495057;
}
}
.job-desc {
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
-webkit-line-clamp: 2;
max-height: 36px;
display: -webkit-box;
-webkit-box-orient: vertical;
}
}
}
}
// top_cities
.top_cities {
.city-item {
margin-bottom: 12px;
a {
font-size: 18px;
text-decoration: none;
color: #287ab9;
margin: 10px 0;
}
}
.all-cities-btn {
color: #287ab9;
font-size: 20px;
text-decoration: none;
}
.all-cities-btn:hover {
text-decoration: underline;
}
}
// top_industries
.top_industries {
.industry-item {
margin-bottom: 12px;
a {
font-size: 18px;
text-decoration: none;
color: #287ab9;
margin: 10px 0;
}
}
.all-industries-btn {
color: #287ab9;
font-size: 20px;
text-decoration: none;
}
.all-industries-btn:hover {
text-decoration: underline;
}
}
class AppliesController < ApplicationController class AppliesController < ApplicationController
before_action :load_job, only: %i[new confirm create] before_action :load_job, only: %i[new confirm create]
before_action :logged_in_user
def index def new
if use_apply_job_session?
build_params = session[:apply_job]
@blob = ActiveStorage::Blob.find(session[:blob_id])
end
@apply = current_user.apply_jobs.build(build_params)
end end
def create def confirm
@user = current_user @apply = current_user.apply_jobs.build(apply_params)
@apply = @job.apply_jobs.build(apply_params) @apply.job = @job
@apply.user_id = @user.id
@apply.cv.attach(params[:apply_job][:cv]) if @apply.valid?
if @apply.save @blob = ActiveStorage::Blob.create_and_upload!(
UserMailer.apply_job(@user, @job, @apply).deliver_now io: apply_params[:cv],
flash.now[:info] = 'Job application information has been sent to your email' filename: apply_params[:cv].original_filename,
content_type: apply_params[:cv].content_type
)
session[:blob_id] = @blob.id
session[:apply_job] = @apply
else else
render 'new' render 'new'
end end
end end
def show def create
end @apply = current_user.apply_jobs.build(apply_params)
@apply.job = @job
def new @apply.cv = ActiveStorage::Blob.find(session[:blob_id])
@apply = @job.apply_jobs.build
end
def confirm if @apply.save
@user = current_user UserMailer.apply_job(current_user, @job, @apply).deliver_now
@apply = @job.apply_jobs.build(apply_params) flash.now[:info] = 'Job application information has been sent to your email'
@apply.user_id = @user.id
if @apply.valid?
@name = params[:apply_job][:user_name]
@email = params[:apply_job][:email]
@cv = params[:apply_job][:cv]
else else
render 'new' render 'new'
end end
session[:blob_id] = nil
session[:apply_job] = nil
end end
private private
def load_job def load_job
@job = Job.find_by(id: params[:job_id]) job_id = params[:job_id] || params[:apply_job][:job_id]
@job = Job.find_by(id: job_id)
render partial: 'shared/job_not_found' if @job.nil?
end end
def apply_params def apply_params
params.require(:apply_job).permit(:user_name, :email, :cv) params.require(:apply_job).permit(:user_name, :email, :cv, :job_id)
end
def current_user
@current_user ||= User.first
end
def use_apply_job_session?
request.referer&.include?('confirm') && session[:apply_job].present?
end end
end end
class CitiesController < ApplicationController class CitiesController < ApplicationController
def index def index
@region_job_list = City.jobs_count @regions = City.regions.keys
end @job_quantity_by_region = City.job_quantity_by_region
def show
end end
end end
class IndustriesController < ApplicationController class IndustriesController < ApplicationController
def index def index
# @industries_job_list = Industry.joins(:jobs).group(:name).count.sort_by(&:first) @job_quantity_by_industry = Industry.job_quantity_by_industry
@industries_job_list = Industry.jobs_count
end
def show
end end
end end
class JobsController < ApplicationController class JobsController < ApplicationController
def index def index
if params[:city_slug].present? if job_params.present?
city = City.find_by(slug: params[:city_slug]) search
@name = city.name
@jobs = city.jobs
elsif params[:industry_slug].present?
industry = Industry.find_by(slug: params[:industry_slug])
@name = industry.name
@jobs = industry.jobs
else else
@name = 'JOBS' @jobs = Job.sort_by_date(page: params[:page], per_page: Job::JOB_PER_PAGE)
@jobs = Job.all
end end
@jobs = @jobs.page(params[:page]).per(20)
end end
def show def show
@job = Job.find(params[:id]) @job = Job.find_by(id: params[:id])
render partial: 'shared/job_not_found' if @job.nil?
end
private
def search
if job_params.key?(:model) && job_params.key?(:slug) # search by model
model = params[:model].classify.constantize
@target = model.find_by(slug: job_params[:slug])
@jobs = @target.jobs.sort_by_date(page: params[:page], per_page: Job::JOB_PER_PAGE)
end
end
def job_params
params.permit(:model, :slug, :search)
end end
end end
class TopController < ApplicationController class TopController < ApplicationController
def index def index
@total_job = Job.count @total_job = Job.count
@latest_jobs = Job.latest @latest_jobs = Job.sort_by_date(per_page: Job::LATEST_JOBS_LIMIT)
@top_cities = City.top_jobs @top_cities = City.top_jobs
@top_industries = Industry.top_jobs @top_industries = Industry.top_jobs
end end
......
...@@ -4,7 +4,26 @@ module ApplicationHelper ...@@ -4,7 +4,26 @@ module ApplicationHelper
if page_title.empty? if page_title.empty?
base_title base_title
else else
page_title + " | " + base_title "#{page_title} | #{base_title}"
end end
end end
def show_location(list)
list.map(&:name).join(' | ')
end
def show_breadcrumb(list, model)
a = list.map do |item|
link_to item.name, job_list_path(model: model, slug: item.slug), class: 'mx-1 text-decoration-none text-info'
end
a.join('|').html_safe
end
# breadcrumb
# def show_breadcrumb(job)
# a = link_to 'Home', root_path, class: 'text-decoration-none text-info mx-1 '
# span = tag.span '>', class: 'mx-1'
# arr = [a, show_item(job.cities, :city), show_item(job.industries, :industry), job.title]
# arr.join(span).html_safe
# end
end end
module CitiesHelper
def get_cities_list(region_name)
City.joins(:jobs).where(region: region_name).group(:name).count
end
end
...@@ -15,8 +15,4 @@ ActiveStorage.start() ...@@ -15,8 +15,4 @@ ActiveStorage.start()
import "bootstrap" import "bootstrap"
window.bootstrap = require("bootstrap"); window.bootstrap = require("bootstrap");
import "../stylesheets/application.scss"; import "../stylesheets/application.scss";
//= require jquery import "./validation";
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .
Validator = function(options) {
var selectorRules = {};
// validate
function validate(inputElement, rule) {
var errorMessage;
var errorElement = inputElement.parentElement.querySelector(options.errorSelector);
// get rules of selector
var rules = selectorRules[rule.selector];
// stop if error
for (var i = 0; i < rules.length; ++i) {
errorMessage = rules[i](inputElement.value)
if (errorMessage) break;
}
if (errorMessage) {
errorElement.innerText = errorMessage
inputElement.parentElement.classList.add('invalid')
} else {
errorElement.innerText = ''
inputElement.parentElement.classList.remove('invalid')
}
return !errorMessage;
}
// get Element
var formElement = document.querySelector(options.form)
if (formElement) {
// handle submit
formElement.onsubmit = function (e) {
e.preventDefault();
var isFormValid = true;
options.rules.forEach(function (rule){
var inputElement = formElement.querySelector(rule.selector);
var isValid = validate(inputElement, rule);
if (!isValid) {
isFormValid = false;
}
});
if (isFormValid) {
formElement.submit()
}
}
options.rules.forEach(function (rule){
// save rules
if (Array.isArray(selectorRules[rule.selector])) {
selectorRules[rule.selector].push(rule.test)
} else {
selectorRules[rule.selector] = [rule.test];
}
var inputElement = formElement.querySelector(rule.selector);
if (inputElement) {
// blur
inputElement.onblur = function () {
validate(inputElement, rule)
}
// when input
inputElement.oninput = function () {
var errorElement = inputElement.parentElement.querySelector(options.errorSelector)
errorElement.innerText = ''
inputElement.parentElement.classList.remove('invalid')
}
}
});
}
}
Validator.isRequired = function (selector) {
return {
selector: selector,
test: function (value) {
return value.trim() ? undefined : "This field can't be blank"
}
}
}
Validator.isEmail = function (selector) {
return {
selector: selector,
test: function (value) {
var regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regex.test(value) ? undefined : 'Email invalid';
}
}
}
Validator.fileTooLarge = function (selector) {
return {
selector: selector,
test: function (value) {
$(selector).on('change', function () {
const size = (this.files[0].size / 1024 / 1024).toFixed(2);
if (size > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.')
$(selector).val('');
}
})
}
}
}
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
include ApplicationHelper
helper :application
default from: 'from@example.com' default from: 'from@example.com'
layout 'mailer' layout 'mailer'
end end
class ApplyJob < ApplicationRecord class ApplyJob < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
ACCEPT_CONTENT_TYPE = 'application/pdf, application/msword, application/zip, application/xls, application/xlsx'.freeze
belongs_to :job belongs_to :job
belongs_to :user belongs_to :user
...@@ -8,9 +9,10 @@ class ApplyJob < ApplicationRecord ...@@ -8,9 +9,10 @@ class ApplyJob < ApplicationRecord
validates :user_name, presence: true, length: { maximum: 50 } validates :user_name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }, validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX } format: { with: VALID_EMAIL_REGEX }
validates :cv, presence: true, validates :cv, presence: true, content_type:
content_type: { in: %w[application/pdf application/msword application/zip application/xls application/xlsx], { in: ACCEPT_CONTENT_TYPE,
message: 'must be a valid cv format' }, message: 'must be a valid cv format' },
size: { less_than: 5.megabytes, size:
{ less_than: 5.megabytes,
message: 'should be less than 5MB' } message: 'should be less than 5MB' }
end end
class City < ApplicationRecord class City < ApplicationRecord
scope :jobs_count, -> { joins(:jobs).group(:region).count } TOP_JOB_COUNT = 9
has_and_belongs_to_many :jobs
enum region: { vietnam: 0, international: 1 } enum region: { vietnam: 0, international: 1 }
has_and_belongs_to_many :jobs
validates :slug, presence: true, uniqueness: { case_sensitive: true } validates :slug, presence: true, uniqueness: { case_sensitive: true }
def to_params def self.top_jobs
slug joins(:jobs).group(:name, :slug).order('COUNT(jobs.id) DESC').count.take(TOP_JOB_COUNT)
end end
def add_slugs def self.job_quantity_by_region
update slug: to_slug(name) regions.each_with_object({}) do |(name, _), hash|
hash[name.to_sym] = send(name).joins(:jobs).group(:name, :slug).count
end end
def self.top_jobs
self.all.map { |city| [city, city.jobs.count] }.sort_by(&:second).reverse.take(9)
end end
end end
class Industry < ApplicationRecord class Industry < ApplicationRecord
scope :jobs_count, -> { joins(:jobs).group(:name).count.sort_by(&:first) } TOP_JOB_COUNT = 9
has_and_belongs_to_many :jobs has_and_belongs_to_many :jobs
validates :slug, presence: true, uniqueness: { case_sensitive: true } validates :slug, presence: true, uniqueness: { case_sensitive: true }
# before_validation :add_slugs def self.top_jobs
joins(:jobs).group(:name, :slug).order('COUNT(jobs.id) DESC').count.take(TOP_JOB_COUNT)
def to_params
slug
end
def add_slugs
update slug: to_slug(name)
end end
def self.top_jobs def self.job_quantity_by_industry
self.all.map { |industry| [industry, industry.jobs.count] }.sort_by(&:second).reverse.take(9) joins(:jobs).group(:name, :slug).count
end end
end end
class Job < ApplicationRecord class Job < ApplicationRecord
scope :latest, -> { order(updated_at: :desc).limit(5).joins(:cities).includes(:company, :cities, :cities_jobs) } LATEST_JOBS_LIMIT = 5
JOB_PER_PAGE = 20
has_and_belongs_to_many :industries has_and_belongs_to_many :industries
has_and_belongs_to_many :cities has_and_belongs_to_many :cities
...@@ -7,4 +8,8 @@ class Job < ApplicationRecord ...@@ -7,4 +8,8 @@ class Job < ApplicationRecord
has_many :apply_jobs, dependent: :destroy has_many :apply_jobs, dependent: :destroy
has_many :favorite_jobs, dependent: :destroy has_many :favorite_jobs, dependent: :destroy
has_many :history_job, dependent: :destroy has_many :history_job, dependent: :destroy
def self.sort_by_date(page: 1, per_page: limit)
includes(:cities, :cities_jobs, :company).order(created_at: :desc).page(page).per(per_page).references(:cities)
end
end end
.ribbon.my-5
ul
li class=("item #{'active' if action_name == 'new'}")
span.circle
| 1
| Apply
li class=("item #{'active' if action_name == 'confirm'}")
span.circle
| 2
| Confirm
li class=("item #{'active' if action_name == 'create'}")
span.circle
| 3
| Done
- provide(:title, "Apply job") - provide(:title, 'Confirmation')
.container
= render 'ribbon'
.container .container
h1.my-5.text-center h1.my-5.text-center
| Confirm | Confirmation
= @job.title
.col .col
= form_with(model: @apply, scope: :apply_job, url: done_path(job_id: @job), local: true) do |f| = form_with(model: @apply, scope: :apply_job, url: done_job_path, local: true) do |f|
.mb-3 .row.mb-5
= f.label :name, 'Full name:', class: 'form-label' = f.hidden_field :job_id, value: @job.id
span.mx-2 = f.hidden_field :blob_id, value: @blob.id
= @name .col-2
= f.hidden_field :user_name, value: @name, class: 'form-control' = f.label :name, 'Full name:', class: 'form-label label'
.col-10
.mb-3 span.mx-2.label.form-control
= f.label :email, 'Email:', class: 'form-label' = @apply.user_name
span.mx-2 = f.hidden_field :user_name, value: @apply.user_name
= @email
= f.hidden_field :email, value: @email, class: 'form-control' .row.mb-5
.col-2
span.mb-3.d-block.align-center = f.label :email, 'Email:', class: 'form-label label'
= f.label :cv, 'Cv:', class: 'form-label' .col-10
/ = f.hidden_field :cv, value: @cv span.mx-2.label.form-control
= f.file_field :cv = @apply.email
= f.hidden_field :email, value: @apply.email
span.mx-2
= @cv.original_filename .row.mb-5
= link_to 'Edit', apply_path(job_id: @job.id), class: "btn btn-secondary py-2 px-4" .col-2
= f.submit "Confirm", class: "btn btn-primary" = f.label :cv, 'Cv: ', class: 'form-label label'
.col-10
span.form-control
= url_for(@apply.cv)
= link_to 'Edit', apply_job_path(job_id: @job.id), class: 'btn btn-secondary w-25 btn-height mr-5 my-5'
= f.submit 'Confirm', class: 'btn btn-primary w-25 btn-height mx-5 my-5'
- provide(:title, 'Done')
.container
=render 'ribbon'
.container.text-center .container.text-center
h3.my-4 h3.my-4
| You have successfully applied for the #{@job.title} | You have successfully applied for the #{@job.title}
......
- provide(:title, "Apply job") - provide(:title, 'Apply job')
.container
= render 'ribbon'
.container .container
h1.my-5.text-center h1.my-5.text-center
| Apply for | Apply Form
p.fs-5.fw-bold.mb-4.text-center
= @job.title = @job.title
.col .col
= form_with(model: @apply, scope: :apply_job, url: confirm_path(job_id: @job), local: true) do |f| = form_with(model: @apply, url: confirm_job_path, local: true, id: 'apply-form') do |f|
= render 'shared/error_messages', object: f.object = render 'shared/error_messages', object: f.object
.mb-3 .row.mb-5.form-group
= f.label :user_name, 'Full name', class: 'form-label' = f.hidden_field :job_id, value: @job.id
= f.text_field :user_name, class: 'form-control' .col-2
.mb-3 = f.label :user_name, 'Full name', class: 'form-label label'
= f.label :email, class: 'form-label' .col-10
= f.text_field :email, class: 'form-control' = f.text_field :user_name, value: @apply.user_name || @current_user.name, class: 'form-control'
span.mb-3.d-block.align-center span.form-msg
= f.file_field :cv .row.mb-5.form-group
= f.submit "Submit", class: "btn btn-primary" .col-2
= f.label :email, class: 'form-label label'
.col-10
= f.text_field :email, value: @apply.email || @current_user.email, class: 'form-control'
span.form-msg
.row.mb-5.form-group
/ - if @user.cv.attached?
/ = url_for(@user.cv)
.col-2
= f.label :cv, class: 'form-label label'
.col-10
= f.file_field :cv, files: @blob, accept: ApplyJob::ACCEPT_CONTENT_TYPE, class: 'form-control'
span.form-msg
br
= f.submit 'Confirm', class: 'btn btn-primary w-25 my-4 btn-height',data: { disable_with: false }
javascript:
Validator({
form: '#apply-form',
errorSelector: '.form-msg',
rules: [
Validator.isRequired('#apply_job_user_name'),
Validator.isRequired('#apply_job_email'),
Validator.isRequired('#apply_job_cv'),
Validator.fileTooLarge('#apply_job_cv'),
Validator.isEmail('#apply_job_email')
]
})
.cities-list
.container
- @region_job_list.each do |region, job_count|
.countries-job-list id=region
.container-fullwidth.py-5
h2.mt-0
= region
small.text-muted
| (#{pluralize(job_count, "job")})
hr.my-4
.row
- list = get_cities_list(region)
- list.each do |city, count|
.col-lg-4.col-md-6.text-center
.mt-5
.city-item
h3.h4.mb-2.see-more-text= link_to city, city_jobs_path(City.find_by(name: city).slug) , class:'text-decoration-none fw-normal text-reset'
p.text-muted.mb-0= pluralize(count, "job")
\ No newline at end of file
.countries-list
.container.py-5
h2.mt-0 REGION
hr.my-4
.row
- @region_job_list.each do |region, job_count|
.col-lg-4.col-md-6.text-center
.mt-5
h3.h4.mb-2.text-white.see-more-text= link_to region, anchor: region
p.text-muted.mb-0= pluralize(job_count, "job")
= render 'regions_list' /region
= render 'cities_list' .countries-list
\ No newline at end of file .container.py-5
h2.mt-0 REGION
hr.my-4
.row
- @regions.each do |region|
.col-lg-4.col-md-6.text-center
.mt-5
h3.h4.mb-2.text-white.see-more-text= link_to region.upcase, anchor: region
// cities list
.cities-list
.container
- @job_quantity_by_region.each do |region, city_job_count|
.countries-job-list id=region
.container-fullwidth.py-5
h2.mt-0
= region.upcase
hr.my-4
.row
- city_job_count.each do |city, count|
.col-lg-4.col-md-6.text-center
.mt-5
.city-item
h3.h4.mb-2.see-more-text= link_to city[0], job_list_path(model: :city, slug: city[1]) , class:'text-decoration-none fw-normal text-reset'
p.text-muted.mb-0= pluralize(count, 'job')
\ No newline at end of file
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
h2.mt-0 INDUSTRIES h2.mt-0 INDUSTRIES
hr.my-4 hr.my-4
#info-box.row #info-box.row
- @industries_job_list.each do |name, job_count| - @job_quantity_by_industry.each do |name, job_count|
.col-lg-6.col-md-6.text-center .col-lg-6.col-md-6.text-center
.mt-5 .mt-5
h3.h4.mb-2.see-more-text= link_to name, industry_jobs_path(Industry.find_by(name: name).slug) , class:'text-decoration-none fw-normal text-reset' h3.h4.mb-2.see-more-text= link_to name[0], job_list_path(model: :industry, slug: name[1]) , class:'text-decoration-none fw-normal text-reset'
p.text-muted.mb-0= pluralize(job_count, "job") p.text-muted.mb-0= pluralize(job_count, 'job')
hr.divider.my-4 hr.divider.my-4
.breadcrumb
= link_to root_path, class: 'text-decoration-none text-info' do
'Home
span.d-block.mx-2
| >
- city_list = @job.cities
- city_list.each_with_index do |city, i|
= link_to city_jobs_path(city.slug), class: 'text-decoration-none text-info' do
= city.name
= " | " if i != city_list.length - 1
span.d-block.mx-2
| >
- industry_list = @job.industries
- industry_list.each_with_index do |industry, i|
= link_to industry_jobs_path(industry.slug), class: 'text-decoration-none text-info' do
= industry.name
= " | " if i != industry_list.length - 1
span.d-block.mx-2
| >
= @job.title
\ No newline at end of file
.job-detail.my5
.job-apply.d-flex.align-items-center.justify-content-between
h2.align-items-start
= @job.title
= link_to 'Apply for this job', apply_path(job_id: Job.find_by(title: @job.title).id), class: 'btn btn-primary'
p.text-secondary
= @job.company.name
.row.bg-light
.col-4
ul.list-unstyled
li
strong
| Location
- @job.cities.each do |city|
p
= city.name
li
strong
| Salary
p.text-success
= @job.salary
.col-4
ul.list-unstyled
li
strong
| Type
p
= @job.job_type
li
strong
| Position
p
= @job.position
.col-4
ul.list-unstyled
li
strong
| Experience
p
= @job.experience
li
strong
| Expiration date
p
= @job.expiration_date
.job-benefits.my-4
h3
| Benefits
.row
- @job.benefit.split('---').each do |benefit|
li.list-unstyled.col-4.text-secondary
= benefit
.job-desc.my-4
h3
| Description
= @job.description.html_safe()
.job-req.my-4
h3
| Requirement
= @job.requirement.html_safe()
.job-info.my-4
h3
| Other info
- @job.other_info.split('---').each do |info|
li.text-secondary
= info
.job-apply.d-flex.align-items-center.justify-content-between
= link_to 'Apply for this job', apply_path(job_id: Job.find_by(title: @job.title).id), class: 'btn btn-primary'
= link_to 'Favorite', '#', class: 'btn btn-primary'
.container
- @jobs.each do |job|
.job-item
= link_to job.title, job_path(Job.find_by(title: job.title).id), class: 'job-title fs-3 text-decoration-none text-reset'
.job-caption
= link_to job.company.name, "#", class: "job-company text-decoration-none text-secondary"
p.job-salary.text-success
| Salary: #{job.salary}
ul.list-unstyled
- job.cities.each do |city|
li
= city.name
.job-desc
= truncate(simple_format(job.description), escape: false, length: 250)
hr.my-4
\ No newline at end of file
- provide(:title, 'Job list page') - provide(:title, 'Job list page')
= render 'search' / search box
= render 'shared/search'
.container .container
h1.mt-5 h1.mt-5
= @name = @name
...@@ -10,7 +11,20 @@ ...@@ -10,7 +11,20 @@
.page-info.p-2 .page-info.p-2
= paginate @jobs = paginate @jobs
.container .container
= render 'jobs' .container
- @jobs.each do |job|
/ job
.job-item
= link_to job.title, job, class: 'job-title fs-3 text-decoration-none text-reset'
.job-caption
= link_to job.company.name, '#', class: 'job-company text-decoration-none text-secondary'
p.job-salary.text-success
| Salary: #{job.salary}
ul.list-unstyled
= show_location(job.cities)
.job-desc
= truncate(simple_format(job.description), escape: false, length: 250)
hr.my-4
.no-padding.d-flex.align-items-center.flex-column .no-padding.d-flex.align-items-center.flex-column
.page-info.p-2 .page-info.p-2
= page_entries_info @jobs = page_entries_info @jobs
......
ruby:
city_list = @job.cities
industry_list = @job.industries
/ html
.container.my-5 .container.my-5
= render 'breadcrumb' / breadcrumb
.breadcrumb
= link_to 'Home', root_path, class: 'text-decoration-none text-info'
span.mx-1 >
.text-info
= show_breadcrumb(city_list, :city)
span.mx-1 >
.text-info
= show_breadcrumb(industry_list, :industry)
span.mx-1 >
= @job.title
hr.my-4 hr.my-4
= render 'job_details'
\ No newline at end of file / job details
.job-detail.my5
.job-apply.d-flex.align-items-center.justify-content-between
h2.align-items-start
= @job.title
= link_to 'Apply for this job', apply_job_path(job_id: @job.id), class: 'btn btn-primary'
p.text-secondary
= @job.company.name
.row.bg-light
.col-4
ul.list-unstyled
li
strong.d-block
| Location
p
= show_location(city_list)
li
strong
| Salary
p.text-success
= @job.salary
.col-4
ul.list-unstyled
li
strong
| Type
p
= @job.job_type
li
strong
| Position
p
= @job.position
.col-4
ul.list-unstyled
li
strong
| Experience
p
= @job.experience
li
strong
| Expiration date
p
= @job.expiration_date.strftime('%d/%m/%Y')
.job-benefits.my-4
h3
| Benefits
.row
- @job.benefit.split('---').each do |benefit|
li.list-unstyled.col-4.text-secondary
= benefit
.job-desc.my-4
h3
| Description
= @job.description.html_safe
.job-req.my-4
h3
| Requirement
= @job.requirement.html_safe
.job-info.my-4
h3
| Other info
- @job.other_info.split('---').each do |info|
li.text-secondary
= info
.job-apply.d-flex.align-items-center.justify-content-between
= link_to 'Apply for this job', apply_job_path(job_id: @job.id), class: 'btn btn-primary'
= link_to 'Favorite', '#', class: 'btn btn-primary'
footer.footer footer.footer
small small
| The | The
= link_to "VenJob", root_path = link_to 'VenJob', root_path
| by Mai Hoàng Thái Hà | by Mai Hoàng Thái Hà
nav nav
ul ul
li li
= link_to "About", '#' = link_to 'About', '#'
li li
= link_to "Contact", '#' = link_to 'Contact', '#'
\ No newline at end of file \ No newline at end of file
header.navbar.navbar-fixed-top.navbar-inverse header.navbar.navbar-fixed-top.navbar-inverse
.container .container
= link_to image_tag("logo.png", alt: "Zigexn logo", width: "150"), = link_to image_tag('logo.png', alt: 'Zigexn logo', width: '150'),
- root_path - root_path
nav nav
ul.nav.navbar-nav.navbar-right.text-light ul.nav.navbar-nav.navbar-right.text-light
...@@ -14,4 +14,3 @@ header.navbar.navbar-fixed-top.navbar-inverse ...@@ -14,4 +14,3 @@ header.navbar.navbar-fixed-top.navbar-inverse
= link_to "Log in", new_user_session_path = link_to "Log in", new_user_session_path
li li
= link_to "Sign up", new_user_registration_path = link_to "Sign up", new_user_registration_path
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js"> <script src='//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js'>
</script> </script>
<![endif]--> <![endif]-->
\ No newline at end of file
...@@ -2,8 +2,8 @@ doctype html ...@@ -2,8 +2,8 @@ doctype html
html html
head head
title title
=full_title(yield(:title)) = full_title(yield(:title))
meta[name="viewport" content="width=device-width,initial-scale=1" charset="utf-8"] meta[name='viewport' content='width=device-width,initial-scale=1' charset='utf-8']
= csrf_meta_tags = csrf_meta_tags
= csp_meta_tag = csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
......
doctype html doctype html
html html
head head
meta[http-equiv="Content-Type" content="text/html; charset=utf-8"] meta[http-equiv='Content-Type' content='text/html; charset=utf-8']
style style
| /* Email styles need to be inline */ | /* Email styles need to be inline */
body body
......
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
- object.errors.full_messages.each do |msg| - object.errors.full_messages.each do |msg|
li.text-danger li.text-danger
= msg = msg
\ No newline at end of file
link crossorigin="anonymous" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" rel="stylesheet" /
.container
.row
.col-md-12
.error-template.text-center
h1.my-4
| Oops!
h2.my-2
| Job Not Found
.error-details.mb-5
| Sorry, an error has occured, Requested page not found!
.error-actions
= link_to 'Take Me Home', root_path, class: 'btn btn-primary btn-lg mx-2'
= link_to 'Contact Support', '#', class: 'btn btn-secondary mx-2 btn-lg'
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.row .row
.col-md-10.mb-md-0.no-padding .col-md-10.mb-md-0.no-padding
span.fa.fa-search.form-control-feedback span.fa.fa-search.form-control-feedback
= search_field_tag :search, params[:search], placeholder: "Find a job", class: "form-control rounded-left no-border-radius bg-light h-100" = search_field_tag :search, params[:search], placeholder: 'Find a job', class: 'form-control rounded-left no-border-radius bg-light h-100'
.col-md-2.mb-md-0.no-padding .col-md-2.mb-md-0.no-padding
= button_tag "", class: "h-100 w-100 btn btn-block btn-lg btn-info ", name: nil = button_tag '', class: 'h-100 w-100 btn btn-block btn-lg btn-info', name: nil
| Search | Search
\ No newline at end of file
.latest-job.mb-5
.container.mb-5
h2
| Latest jobs
hr.my-2
- @latest_jobs.each do |job|
.job-item.mb-4
= link_to job.title, job_path(Job.find_by(title: job.title).id), class: "job-title"
.job-caption
= link_to job.company.name, "#", class: "job-company"
p.job-salary
| Salary:
= job.salary
.job-locations
ul
- job.cities.each do |city|
li
= city.name
.job-desc
= truncate(simple_format(job.description), escape: false, length: 250)
hr.my-2
.search.text-center.mb-5
.overlay
.container
.row
.col-xl-9.mx-auto
h1.mb-5.mt-5
| Total:
= pluralize(@total_job, "job")
.row.justify-content-start
.col-md-10.mb-md-0.no-padding
span.fa.fa-search.form-control-feedback
= search_field_tag :search, params[:search], placeholder: "Find a job", class: "form-control rounded-left no-border-radius bg-light h-100"
.col-md-2.mb-md-0.no-padding
= button_tag "", class: "h-100 w-100 btn btn-block btn-lg btn-info fa fa-search rounded-right no-border-radius ", name: nil
| Search
\ No newline at end of file
.top_cities.mb-5
.container
h2 Top cities
hr.my-2
.row.align-items-start.mb-3
- @top_cities.each do |city, city_jobs|
.col-4.city-item
= link_to city.name, city_jobs_path(City.find_by(name: city.name).slug), class: 'city-name'
span.job-count
| (
= pluralize(city_jobs, "job")
|)
= link_to 'See all cities', cities_path, class:'all-cities-btn'
\ No newline at end of file
.top_industries.mb-5
.container
h2 Top industries
hr.my-2
.row.align-items-start
- @top_industries.each do |industry, industry_jobs|
.col-4.industry-item
= link_to industry.name, industry_jobs_path(Industry.find_by(name: industry.name).slug), class: 'industry-name'
span.job-count
| (
= pluralize(industry_jobs, "job")
|)
= link_to 'See all industries', industries_path, class:'all-industries-btn'
\ No newline at end of file
- provide(:title, 'Home page') - provide(:title, 'Home page')
= render 'search' / search box
= render 'latest_jobs' .container
= render 'top_cities' .banner.d-flex.justify-content-center.align-items-center
= render 'top_industries' .row
.col
h2.text-white
| Total: #{pluralize(@total_job, "job")}
= render 'shared/search'
/ latest job
.latest-job.mb-5
.container.mb-5
h2
| Latest jobs
hr.my-2
- @latest_jobs.each do |job|
.job-item.mb-4
= link_to job.title, job, class: 'job-title'
.job-caption
= link_to job.company.name, '#', class: 'job-company'
p.job-salary
| Salary:
= job.salary
.job-locations
- city_list = job.cities
= show_location(job.cities)
.job-desc
= truncate(simple_format(job.description), escape: false, length: 250)
hr.my-2
/ top cities
.top_cities.mb-5
.container
h2 Top cities
hr.my-2
.row.align-items-start.mb-3
- @top_cities.each do |city, city_jobs|
.col-4.city-item
= link_to city[0], job_list_path(model: :city, slug: city[1]), class: 'city-name'
span.job-count
| (
= pluralize(city_jobs, 'job')
|)
= link_to 'See all cities', cities_path, class:'all-cities-btn'
/ top industries
.top_industries.mb-5
.container
h2 Top industries
hr.my-2
.row.align-items-start
- @top_industries.each do |industry, industry_jobs|
.col-4.industry-item
= link_to industry[0], job_list_path(model: :industry, slug: industry[1]), class: 'industry-name'
span.job-count
| (
= pluralize(industry_jobs, 'job')
|)
= link_to 'See all industries', industries_path, class:'all-industries-btn'
\ No newline at end of file
...@@ -6,8 +6,7 @@ p ...@@ -6,8 +6,7 @@ p
| Job title: #{@job.title} | Job title: #{@job.title}
p p
| Location: | Location:
- @job.cities.each do |city| = show_location(@job.cities)
span= city.name
p p
| Company: #{@job.company.name} | Company: #{@job.company.name}
br/ br/
...@@ -16,5 +15,6 @@ p ...@@ -16,5 +15,6 @@ p
| Full name: #{@apply.user_name} | Full name: #{@apply.user_name}
p p
| Email: #{@apply.email} | Email: #{@apply.email}
p CV: p
| CV: #{url_for(@apply.cv)}
p Best, p Best,
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
= "\n" = "\n"
' Job title: #{@job.title} ' Job title: #{@job.title}
= "\n" = "\n"
' Location: #{show_location(@job.cities)}
= "\n"
' Company: #{@job.company.name} ' Company: #{@job.company.name}
= "\n" = "\n"
' Your submitted information: ' Your submitted information:
...@@ -14,6 +16,6 @@ ...@@ -14,6 +16,6 @@
= "\n" = "\n"
' Email: #{@apply.email} ' Email: #{@apply.email}
= "\n" = "\n"
' CV: #{@apply.cv} ' CV: #{url_for(@apply.cv)}
= "\n" = "\n"
' Best, ' Best,
...@@ -10,7 +10,6 @@ module VenJob ...@@ -10,7 +10,6 @@ module VenJob
class Application < Rails::Application class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version. # Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1 config.load_defaults 6.1
# Configuration for the application, engines, and railties goes here. # Configuration for the application, engines, and railties goes here.
# #
# These settings can be overridden in specific environments using the files # These settings can be overridden in specific environments using the files
......
...@@ -96,4 +96,8 @@ Rails.application.configure do ...@@ -96,4 +96,8 @@ Rails.application.configure do
Bullet.rails_logger = true Bullet.rails_logger = true
Bullet.add_footer = true Bullet.add_footer = true
end end
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 }
config.action_mailer.raise_delivery_errors = false
end end
class String class String
def to_slug def to_slug
source = [ source = %w[
'à', 'á', 'â', 'ã', 'è', 'é', 'ê', 'ì', 'í', 'ò', à á â ã è é ê ì í ò ó ô õ ù ú ý ă đ ĩ ũ ơ ư ạ ả ấ ầ ẩ ẫ ậ ắ ằ ẳ ẵ ặ
'ó', 'ô', 'õ', 'ù', 'ú', 'ý', 'ă', 'đ', 'ĩ', 'ũ', ẹ ẻ ẽ ế ề ể ễ ệ ỉ ị ọ ỏ ố ồ ổ ỗ ộ ớ ờ ở ỡ ợ ụ ủ ứ ừ ử ữ ự ỳ ỷ ỹ ỵ
'ơ', 'ư', 'ạ', 'ả', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ắ', ].freeze
'ằ', 'ẳ', 'ẵ', 'ặ', 'ẹ', 'ẻ', 'ẽ', 'ế', 'ề', 'ể', destination = %w[
'ễ', 'ệ', 'ỉ', 'ị', 'ọ', 'ỏ', 'ố', 'ồ', 'ổ', 'ỗ', a a a a e e e i i o o o o u u y a d i u o u a a a a a a a a a a a a
'ộ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ụ', 'ủ', 'ứ', 'ừ', e e e e e e e e i i o o o o o o o o o o o o u u u u u u u y y y y
'ử', 'ữ', 'ự', 'ỳ', 'ỷ', 'ỹ', 'ỵ' ].freeze
]
destination = [
'a', 'a', 'a', 'a', 'e', 'e', 'e', 'i', 'i', 'o',
'o', 'o', 'o', 'u', 'u', 'y', 'a', 'd', 'i', 'u',
'o', 'u', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
'a', 'a', 'a', 'a', 'e', 'e', 'e', 'e', 'e', 'e',
'e', 'e', 'i', 'i', 'o', 'o', 'o', 'o', 'o', 'o',
'o', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u',
'u', 'u', 'u', 'y', 'y', 'y', 'y'
]
hash = Hash[source.zip destination] hash = Hash[source.zip destination]
self.downcase.encode('ASCII', 'UTF-8', fallback: hash).gsub(' ', '-').parameterize.truncate 80 downcase.encode('ASCII', 'UTF-8', fallback: hash).gsub(' ', '-').parameterize
end end
end end
...@@ -2,18 +2,15 @@ Rails.application.routes.draw do ...@@ -2,18 +2,15 @@ Rails.application.routes.draw do
devise_for :users devise_for :users
root 'top#index' root 'top#index'
resources :cities, param: :slug, only: %i[index show] do resources :cities, only: %i[index]
resources :jobs, only: %i[index] resources :industries, only: %i[index]
end get '/jobs/:model/:slug', to: 'jobs#index', as: :job_list
resources :industries, param: :slug, only: %i[index show] do
resources :jobs, only: %i[index]
end
resources :jobs, only: %i[index show] resources :jobs, only: %i[index show]
resources :users, only: %i[index show] resources :users, only: %i[index show]
get '/apply', to: 'applies#new' get '/apply', to: 'applies#new', as: :apply_job
post '/confirm', to: 'applies#confirm' post '/confirm', to: 'applies#confirm', as: :confirm_job
post '/done', to: 'applies#create' post '/done', to: 'applies#create', as: :done_job
end end
class CreateJobs < ActiveRecord::Migration[6.1] class CreateJobs < ActiveRecord::Migration[6.1]
def change def change
create_table :jobs do |t| create_table :jobs do |t|
t.string :title t.string :title, null: false
t.string :job_type t.string :job_type
t.string :salary t.string :salary
t.string :experience t.string :experience
......
require 'open-uri' require 'open-uri'
require 'csv'
require 'zip'
namespace :crawler do namespace :crawler do
# command: rails crawler:jobs TYPE=TEST / ALL
desc 'crawler from CareerBuilder' desc 'crawler from CareerBuilder'
task jobs: :environment do task jobs: :environment do
ARGV.each { |a| task a.to_sym { ; } } unless %w[ALL TEST].include?(ENV['TYPE'])
abort 'Do you want to crawl all pages (ALL) or some pages (TEST)? Please ONLY pass ONE argument.'
total_pages = 0 end
if ARGV.length == 1 && ARGV[0] == 'TEST' logger = Logger.new("#{Rails.root}/log/job_crawler.log")
total_pages = 5 logger.info "Start crawler job at: #{Time.current}"
elsif ARGV.length == 1 && ARGV[0] == 'ALL' total_pages = 5 # default = TEST
if ENV['TYPE'] == 'ALL'
first_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/viec-lam/tat-ca-viec-lam-vi.html').body) first_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/viec-lam/tat-ca-viec-lam-vi.html').body)
jobs_per_page = first_page.css('div.job-item').count jobs_per_page = first_page.css('div.job-item').count
total_jobs = first_page.css('.search-result-list .job-found p').text.split(' ').first.gsub(',', '').to_i total_jobs = first_page.css('.search-result-list .job-found-amout p').text.tr('^0-9', '')
total_pages = (total_jobs.to_f / jobs_per_page).round total_pages = (total_jobs.to_f / jobs_per_page).round
else
exit
end end
(1..total_pages).each do |page| (1..total_pages).each do |page|
parsed_page = Nokogiri::HTML(HTTParty.get("https://careerbuilder.vn/viec-lam/tat-ca-viec-lam-trang-#{page}-vi.html").body) parsed_page = Nokogiri::HTML(HTTParty.get("https://careerbuilder.vn/viec-lam/tat-ca-viec-lam-trang-#{page}-vi.html").body)
logger.info("Page: #{page}")
jobs_item = parsed_page.css('div.job-item .job_link') jobs_item = parsed_page.css('div.job-item .job_link')
jobs_item.each do |item| jobs_item.each do |item|
job_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/vi/tim-viec-lam/' + retries ||= 0
CGI.escape(item.attribute('href').text.remove('https://careerbuilder.vn/vi/tim-viec-lam/'))).body) url ||= item.attribute('href').text
job_detail = job_page.css('section.job-detail-content') logger.info("job link: #{url}")
# title - company job_page = Nokogiri::HTML(HTTParty.get(url).body)
title = job_page.css('div.job-desc h1.title').text # Job
company = job_page.css('div.job-desc a.job-company-name').text job_title = job_page.css('div.job-desc h1.title').text
# info box if job_title.blank?
info_box_item = job_detail.css('.detail-box ul li') logger.info 'Remove this job because title is empty'
# city, update_at, industry, type, salary, experience, level, expiration_date next
job_industries = [] end
# update_at, job_industries, job_type, salary, experience, level, expiration_date
detail_box_items = job_page.css('.job-detail-content .detail-box ul li')
# init
update_at, job_type, salary, experience, level, expiration_date = '' update_at, job_type, salary, experience, level, expiration_date = ''
job_cities = [] industries = []
job_detail.css('.detail-box .map p a').each do |part| detail_box_items.each do |info_item|
city = part.text key = info_item.css('strong').text.strip
job_cities << city default_value = info_item.css('p').text.squish
end # case/when
case key
info_box_item.each do |info_item| when 'Ngày cập nhật'
info = info_item.text update_at = default_value.to_time
if info.include?(key = 'Ngày cập nhật') when 'Ngành nghề'
update_at = info.squish.remove(key).strip industries = default_value.split(' , ')
elsif info.include?(key = 'Ngành nghề') when 'Hình thức'
job_industries = info.squish.remove(key).strip.split(' , ') job_type = default_value
elsif info.include?(key = 'Hình thức') when 'Lương'
job_type = info.squish.remove(key).strip salary = default_value
elsif info.include?(key = 'Lương') when 'Kinh nghiệm'
salary = info.squish.remove(key).strip experience = default_value.squish
elsif info.include?(key = 'Kinh nghiệm') when 'Cấp bậc'
experience = info.squish.remove(key).strip level = default_value
elsif info.include?(key = 'Cấp bậc') when 'Hết hạn nộp'
level = info.squish.remove(key).strip expiration_date = default_value.to_time
elsif info.include?(key = 'Hết hạn nộp') end
expiration_date = info.squish.remove(key).strip end
end # benefits, description, requirement, other_info
end job_detail_rows = job_page.css('section.job-detail-content div.detail-row')
benefits, description, requirement, other_info = ''
# benefit job_detail_rows.each do |detail_row|
benefit_list = [] detail_title = detail_row.css('.detail-title').text.strip
other_info_list = [] detail_content = detail_row.css(':not(h3.detail-title)')
benefits = job_detail.css('ul.welfare-list li') case detail_title
benefits.each do |part| when 'Phúc lợi'
benefit = part.text.strip benefits = detail_row.css('ul.welfare-list li').map(&:text).map(&:squish).join('---')
benefit_list << benefit when 'Mô tả Công việc'
end description = detail_content.inner_html
# description, requirement when 'Yêu Cầu Công Việc'
description, requirement = '' requirement = detail_content.inner_html
job_detail_row = job_detail.css('div.detail-row') when 'Thông tin khác'
job_detail_row.each do |part| other_info = detail_row.css('.content_fck ul li').map(&:text).map(&:squish).join('---')
job_detail_text = part.text end
if job_detail_text.include?('Mô tả Công việc') end
description = job_detail_text.partition('Mô tả Công việc').last.squish.strip # Company
elsif job_detail_text.include?('Yêu Cầu Công Việc') company_name = job_page.css('div.job-desc a.job-company-name').text
requirement = job_detail_text.partition('Yêu Cầu Công Việc').last.squish.strip company_object = Company.find_or_create_by(name: company_name)
end job_object = Job.find_or_create_by({ title: job_title,
end
# other info
other_info = job_detail.css('div.content_fck ul li')
other_info.each do |part|
info = part.text.squish.strip
other_info_list << info
end
company = Company.find_or_create_by(name: company)
job = Job.find_or_create_by(
title: title,
job_type: job_type, job_type: job_type,
salary: salary, salary: salary,
position: level,
experience: experience, experience: experience,
position: level,
expiration_date: expiration_date, expiration_date: expiration_date,
benefit: benefit_list.each { |benefit| },
description: description, description: description,
benefit: benefits,
requirement: requirement, requirement: requirement,
other_info: other_info_list.each { |info| } other_info: other_info,
) company_id: company_object.id })
company.jobs << job industry_objects = industries.map { |industry| Industry.find_or_create_by(name: industry) }
job_industries.each do |industry| job_object.industries << industry_objects
industry_id = Industry.find_or_create_by(name: industry) # Cities
job.industries << industry_id cities = job_page.css('.job-detail-content .detail-box .map p a').map(&:text)
end city_objects = cities.map { |city| City.find_or_create_by(name: city) }
job_cities.each do |city| job_object.cities << city_objects
city_id = City.find_or_create_by(name: city) rescue URI::InvalidURIError => e
job.cities << city_id puts "[Error] #{e.message}"
end logger.error "URI must be ascii only : #{url}"
end encode_url = CGI.escape(url.remove('https://careerbuilder.vn/vi/tim-viec-lam/'))
end url = "https://careerbuilder.vn/vi/tim-viec-lam/#{encode_url}"
retry if (retries += 1) < 2
rescue StandardError => e
puts e.message
puts e.backtrace.inspect
end
end
logger.info "Finished at: #{Time.current}"
end end
desc 'crawler industry form CareerBuilder' desc 'crawler industry form CareerBuilder'
task industries: :environment do task industries: :environment do
parsed_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/tim-viec-lam.html').body) parsed_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/tim-viec-lam.html').body)
list_job = parsed_page.css('div.list-of-working-positions ul.list-jobs li a') list_job = parsed_page.css('div.list-of-working-positions ul.list-jobs li a')
industry_list = []
list_job.each do |part| list_job.each do |part|
industry = part.text.squish.strip industry = part.text.squish.strip
industry_list << industry Industry.find_or_create_by(name: industry)
end
industry_list.each do |industry|
Industry.create(name: industry)
end end
end end
desc 'crawler city form CareerBuilder' desc 'crawler city form CareerBuilder'
task cities: :environment do task cities: :environment do
parsed_page ||= Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/tim-viec-lam.html').body) parsed_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/tim-viec-lam.html').body)
list_location = parsed_page.css('div.main-jobs-by-location ul li') list_location = parsed_page.css('div.main-jobs-by-location ul li')
city_list = [] list_location.each do |city|
city_name = city.text
list_location.each do |part| region = :international
city_item = part.text if city_name.start_with?('Việc làm tại')
region = 1 city_name = city_name.remove('Việc làm tại').strip
if city_item.include?(key = 'Việc làm tại') region = :vietnam
city_item = city_item.remove(key).strip end
region = 0 City.find_or_create_by(
end name: city_name,
city = {
name: city_item,
region: region region: region
}
city_list << city
end
city_list.each do |city|
City.create(
name: city[:name],
region: city[:region]
) )
end end
end end
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"@rails/ujs": "^6.0.0", "@rails/ujs": "^6.0.0",
"@rails/webpacker": "5.4.0", "@rails/webpacker": "5.4.0",
"bootstrap": "^5.0.0-beta3", "bootstrap": "^5.0.0-beta3",
"bs-stepper": "^1.7.0",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"turbolinks": "^5.2.0", "turbolinks": "^5.2.0",
"webpack": "^4.46.0", "webpack": "^4.46.0",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment