Commit 0c43ff2c by Mai Hoang Thai Ha

merge master to branch task 12

parents 5e079295 c2fd5587
......@@ -31,9 +31,15 @@ gem 'bootsnap', '>= 1.4.4', require: false
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
<<<<<<< HEAD
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'
>>>>>>> master
end
group :development do
......
......@@ -113,7 +113,7 @@ GEM
concurrent-ruby (~> 1.0)
jbuilder (2.11.2)
activesupport (>= 5.0.0)
listen (3.5.1)
listen (3.6.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.10.0)
......@@ -180,7 +180,7 @@ GEM
rake (>= 0.13)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.3)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
......@@ -195,7 +195,7 @@ GEM
rubocop-ast (>= 1.7.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0)
rubocop-ast (1.8.0)
parser (>= 3.0.1.1)
rubocop-rails (2.11.3)
activesupport (>= 4.2.0)
......@@ -220,7 +220,7 @@ GEM
slim (4.1.0)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
slim-rails (3.2.0)
slim-rails (3.3.0)
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 5.0)
......@@ -279,7 +279,11 @@ DEPENDENCIES
mysql2 (~> 0.5)
nokogiri (~> 1.11, >= 1.11.7)
pry (~> 0.14.1)
<<<<<<< HEAD
pry-rails (~> 0.3.9)
=======
pry-rails
>>>>>>> master
puma (~> 5.0)
rack-mini-profiler (~> 2.0)
rails (~> 6.1.3, >= 6.1.3.2)
......
// 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";
$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
*{
......@@ -34,12 +23,12 @@ header {
li {
margin-left: 18px;
a {
color: $gray-400;
color: #ced4da;
font-size: 18px;
text-decoration: none;
font-weight: 500;
&:hover{
color: $white;
color: #fff;
}
}
}
......@@ -58,9 +47,9 @@ footer {
color: #777;
a {
text-decoration:none;
color: $gray-600;
color: #6c757d;
&:hover {
color: $gray-900;
color: #212529;
}
}
small {
......@@ -75,96 +64,3 @@ footer {
}
}
}
// Top page
// search
// 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: $gray-700;
}
.job-salary {
color: #008563;
margin: 0;
}
.job-locations {
ul {
list-style: none;
margin: 0 0 6px 0;
padding: 0;
color: $gray-700;
}
}
.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;
}
}
// cities list
\ No newline at end of file
// 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.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Top page
// search
// 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 ApplicationController < ActionController::Base
end
class CitiesController < ApplicationController
def index
@region_job_list = City.jobs_count
end
def show
@regions = City.regions.keys
@job_quantity_by_region = City.job_quantity_by_region
end
end
class IndustriesController < ApplicationController
def index
# @industries_job_list = Industry.joins(:jobs).group(:name).count.sort_by(&:first)
@industries_job_list = Industry.jobs_count
end
def show
@job_quantity_by_industry = Industry.job_quantity_by_industry
end
end
class JobsController < ApplicationController
def index
if params[:city_slug].present?
city = City.find_by(slug: params[:city_slug])
@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
if job_params.present?
search
else
@name = 'JOBS'
@jobs = Job.all
@jobs = Job.sort_by_date(page: params[:page], per_page: Job::JOB_PER_PAGE)
end
@jobs = @jobs.page(params[:page]).per(20)
end
def show
@job = Job.find(params[:id])
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
class TopController < ApplicationController
def index
@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_industries = Industry.top_jobs
end
......
......@@ -4,7 +4,26 @@ module ApplicationHelper
if page_title.empty?
base_title
else
page_title + " | " + base_title
"#{page_title} | #{base_title}"
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
module CitiesHelper
def get_cities_list(region_name)
City.joins(:jobs).where(region: region_name).group(:name).count
end
end
class City < ApplicationRecord
scope :jobs_count, -> { joins(:jobs).group(:region).count }
TOP_JOB_COUNT = 9
enum region: { vietnam: 0, international: 1 }
......@@ -8,6 +8,12 @@ class City < ApplicationRecord
validates :slug, presence: true, uniqueness: { case_sensitive: true }
def self.top_jobs
all.map { |city| [city, city.jobs.count] }.sort_by(&:second).reverse.take(9)
joins(:jobs).group(:name, :slug).order('COUNT(jobs.id) DESC').count.take(TOP_JOB_COUNT)
end
def self.job_quantity_by_region
regions.each_with_object({}) do |(name, _), hash|
hash[name.to_sym] = send(name).joins(:jobs).group(:name, :slug).count
end
end
end
class Industry < ApplicationRecord
scope :jobs_count, -> { joins(:jobs).group(:name).count.sort_by(&:first) }
TOP_JOB_COUNT = 9
has_and_belongs_to_many :jobs
validates :slug, presence: true, uniqueness: { case_sensitive: true }
def self.top_jobs
all.map { |industry| [industry, industry.jobs.count] }.sort_by(&:second).reverse.take(9)
joins(:jobs).group(:name, :slug).order('COUNT(jobs.id) DESC').count.take(TOP_JOB_COUNT)
end
def self.job_quantity_by_industry
joins(:jobs).group(:name, :slug).count
end
end
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 :cities
......@@ -7,4 +8,8 @@ class Job < ApplicationRecord
has_many :apply_jobs, dependent: :destroy
has_many :favorite_jobs, 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
.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'
= render 'cities_list'
\ No newline at end of file
/region
.countries-list
.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 @@
h2.mt-0 INDUSTRIES
hr.my-4
#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
.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'
p.text-muted.mb-0= pluralize(job_count, "job")
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')
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
.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')
= render 'search'
/ search box
= render 'shared/search'
.container
h1.mt-5
= @name
......@@ -10,7 +11,20 @@
.page-info.p-2
= paginate @jobs
.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
.page-info.p-2
= page_entries_info @jobs
......
ruby:
city_list = @job.cities
industry_list = @job.industries
/ html
.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
= 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', '#', 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', '#', class: 'btn btn-primary'
= link_to 'Favorite', '#', class: 'btn btn-primary'
footer.footer
small
| The
= link_to "VenJob", root_path
= link_to 'VenJob', root_path
| by Mai Hoàng Thái Hà
nav
ul
li
= link_to "About", '#'
= link_to 'About', '#'
li
= link_to "Contact", '#'
\ No newline at end of file
= link_to 'Contact', '#'
\ No newline at end of file
header.navbar.navbar-fixed-top.navbar-inverse
.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
nav
ul.nav.navbar-nav.navbar-right
li
= link_to "Log in", '#'
= link_to 'Log in', '#'
li
= link_to "Sign up", '#'
\ No newline at end of file
= link_to 'Sign up', '#'
\ No newline at end of file
<!--[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>
<![endif]-->
\ No newline at end of file
......@@ -2,8 +2,8 @@ doctype html
html
head
title
=full_title(yield(:title))
meta[name="viewport" content="width=device-width,initial-scale=1" charset="utf-8"]
= full_title(yield(:title))
meta[name='viewport' content='width=device-width,initial-scale=1' charset='utf-8']
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
......
doctype html
html
head
meta[http-equiv="Content-Type" content="text/html; charset=utf-8"]
meta[http-equiv='Content-Type' content='text/html; charset=utf-8']
style
| /* Email styles need to be inline */
body
......
......@@ -3,7 +3,7 @@
.row
.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"
= 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 ", name: nil
= button_tag '', class: 'h-100 w-100 btn btn-block btn-lg btn-info', name: nil
| 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')
= render 'search'
= render 'latest_jobs'
= render 'top_cities'
= render 'top_industries'
/ search box
.container
.banner.d-flex.justify-content-center.align-items-center
.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
class String
def to_slug
source = [
'à', 'á', 'â', 'ã', 'è', 'é', 'ê', 'ì', 'í', 'ò',
'ó', 'ô', 'õ', 'ù', 'ú', 'ý', 'ă', 'đ', 'ĩ', 'ũ',
'ơ', 'ư', 'ạ', 'ả', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ắ',
'ằ', 'ẳ', 'ẵ', 'ặ', 'ẹ', 'ẻ', 'ẽ', 'ế', 'ề', 'ể',
'ễ', 'ệ', 'ỉ', 'ị', 'ọ', 'ỏ', 'ố', 'ồ', 'ổ', 'ỗ',
'ộ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ụ', 'ủ', 'ứ', 'ừ',
'ử', 'ữ', 'ự', 'ỳ', 'ỷ', 'ỹ', 'ỵ'
]
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'
]
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
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
Rails.application.routes.draw do
root 'top#index'
resources :cities, param: :slug, only: %i[index show] do
resources :jobs, only: %i[index]
end
resources :industries, param: :slug, only: %i[index show] do
resources :jobs, only: %i[index]
end
resources :cities, only: %i[index]
resources :industries, only: %i[index]
get '/jobs/:model/:slug', to: 'jobs#index', as: :job_list
resources :jobs, only: %i[index show]
......
class CreateJobs < ActiveRecord::Migration[6.1]
def change
create_table :jobs do |t|
t.string :title
t.string :title, null: false
t.string :job_type
t.string :salary
t.string :experience
......
require 'open-uri'
require 'csv'
require 'zip'
namespace :crawler do
# command: rails crawler:jobs TYPE=TEST / ALL
desc 'crawler from CareerBuilder'
task jobs: :environment do
ARGV.each { |a| task a.to_sym { ; } }
total_pages = 0
if ARGV.length == 1 && ARGV[0] == 'TEST'
total_pages = 5
elsif ARGV.length == 1 && ARGV[0] == 'ALL'
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.'
end
logger = Logger.new("#{Rails.root}/log/job_crawler.log")
logger.info "Start crawler job at: #{Time.current}"
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)
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
else
exit
end
(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)
logger.info("Page: #{page}")
jobs_item = parsed_page.css('div.job-item .job_link')
jobs_item.each do |item|
job_page = Nokogiri::HTML(HTTParty.get('https://careerbuilder.vn/vi/tim-viec-lam/' +
CGI.escape(item.attribute('href').text.remove('https://careerbuilder.vn/vi/tim-viec-lam/'))).body)
job_detail = job_page.css('section.job-detail-content')
# title - company
title = job_page.css('div.job-desc h1.title').text
company = job_page.css('div.job-desc a.job-company-name').text
# info box
info_box_item = job_detail.css('.detail-box ul li')
# city, update_at, industry, type, salary, experience, level, expiration_date
job_industries = []
retries ||= 0
url ||= item.attribute('href').text
logger.info("job link: #{url}")
job_page = Nokogiri::HTML(HTTParty.get(url).body)
# Job
job_title = job_page.css('div.job-desc h1.title').text
if job_title.blank?
logger.info 'Remove this job because title is empty'
next
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 = ''
job_cities = []
job_detail.css('.detail-box .map p a').each do |part|
city = part.text
job_cities << city
end
info_box_item.each do |info_item|
info = info_item.text
if info.include?(key = 'Ngày cập nhật')
update_at = info.squish.remove(key).strip
elsif info.include?(key = 'Ngành nghề')
job_industries = info.squish.remove(key).strip.split(' , ')
elsif info.include?(key = 'Hình thức')
job_type = info.squish.remove(key).strip
elsif info.include?(key = 'Lương')
salary = info.squish.remove(key).strip
elsif info.include?(key = 'Kinh nghiệm')
experience = info.squish.remove(key).strip
elsif info.include?(key = 'Cấp bậc')
level = info.squish.remove(key).strip
elsif info.include?(key = 'Hết hạn nộp')
expiration_date = info.squish.remove(key).strip
end
end
# benefit
benefit_list = []
other_info_list = []
benefits = job_detail.css('ul.welfare-list li')
benefits.each do |part|
benefit = part.text.strip
benefit_list << benefit
end
# description, requirement
description, requirement = ''
job_detail_row = job_detail.css('div.detail-row')
job_detail_row.each do |part|
job_detail_text = part.text
if job_detail_text.include?('Mô tả Công việc')
description = job_detail_text.partition('Mô tả Công việc').last.squish.strip
elsif job_detail_text.include?('Yêu Cầu Công Việc')
requirement = job_detail_text.partition('Yêu Cầu Công Việc').last.squish.strip
end
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,
industries = []
detail_box_items.each do |info_item|
key = info_item.css('strong').text.strip
default_value = info_item.css('p').text.squish
# case/when
case key
when 'Ngày cập nhật'
update_at = default_value.to_time
when 'Ngành nghề'
industries = default_value.split(' , ')
when 'Hình thức'
job_type = default_value
when 'Lương'
salary = default_value
when 'Kinh nghiệm'
experience = default_value.squish
when 'Cấp bậc'
level = default_value
when 'Hết hạn nộp'
expiration_date = default_value.to_time
end
end
# benefits, description, requirement, other_info
job_detail_rows = job_page.css('section.job-detail-content div.detail-row')
benefits, description, requirement, other_info = ''
job_detail_rows.each do |detail_row|
detail_title = detail_row.css('.detail-title').text.strip
detail_content = detail_row.css(':not(h3.detail-title)')
case detail_title
when 'Phúc lợi'
benefits = detail_row.css('ul.welfare-list li').map(&:text).map(&:squish).join('---')
when 'Mô tả Công việc'
description = detail_content.inner_html
when 'Yêu Cầu Công Việc'
requirement = detail_content.inner_html
when 'Thông tin khác'
other_info = detail_row.css('.content_fck ul li').map(&:text).map(&:squish).join('---')
end
end
# Company
company_name = job_page.css('div.job-desc a.job-company-name').text
company_object = Company.find_or_create_by(name: company_name)
job_object = Job.find_or_create_by({ title: job_title,
job_type: job_type,
salary: salary,
position: level,
experience: experience,
position: level,
expiration_date: expiration_date,
benefit: benefit_list.each { |benefit| },
description: description,
benefit: benefits,
requirement: requirement,
other_info: other_info_list.each { |info| }
)
company.jobs << job
job_industries.each do |industry|
industry_id = Industry.find_or_create_by(name: industry)
job.industries << industry_id
end
job_cities.each do |city|
city_id = City.find_or_create_by(name: city)
job.cities << city_id
end
end
end
other_info: other_info,
company_id: company_object.id })
industry_objects = industries.map { |industry| Industry.find_or_create_by(name: industry) }
job_object.industries << industry_objects
# Cities
cities = job_page.css('.job-detail-content .detail-box .map p a').map(&:text)
city_objects = cities.map { |city| City.find_or_create_by(name: city) }
job_object.cities << city_objects
rescue URI::InvalidURIError => e
puts "[Error] #{e.message}"
logger.error "URI must be ascii only : #{url}"
encode_url = CGI.escape(url.remove('https://careerbuilder.vn/vi/tim-viec-lam/'))
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
desc 'crawler industry form CareerBuilder'
task industries: :environment do
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')
industry_list = []
list_job.each do |part|
industry = part.text.squish.strip
industry_list << industry
end
industry_list.each do |industry|
Industry.create(name: industry)
Industry.find_or_create_by(name: industry)
end
end
desc 'crawler city form CareerBuilder'
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')
city_list = []
list_location.each do |part|
city_item = part.text
region = 1
if city_item.include?(key = 'Việc làm tại')
city_item = city_item.remove(key).strip
region = 0
end
city = {
name: city_item,
list_location.each do |city|
city_name = city.text
region = :international
if city_name.start_with?('Việc làm tại')
city_name = city_name.remove('Việc làm tại').strip
region = :vietnam
end
City.find_or_create_by(
name: city_name,
region: region
}
city_list << city
end
city_list.each do |city|
City.create(
name: city[:name],
region: city[:region]
)
end
end
......
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