Commit a2003231 by phuctmZigexn

Merge branch 'Task/12_apply_page_ID5/confirm_page_ID6/Done_page/ID7' into 'master'

Feature apply job

See merge request !11
parents c2fd5587 da93ae47
...@@ -63,3 +63,4 @@ gem 'slim-rails', '~> 3.2' ...@@ -63,3 +63,4 @@ gem 'slim-rails', '~> 3.2'
gem 'kaminari', :git => 'https://github.com/kaminari/kaminari' gem 'kaminari', :git => 'https://github.com/kaminari/kaminari'
gem 'nokogiri', '~> 1.11', '>= 1.11.7' gem 'nokogiri', '~> 1.11', '>= 1.11.7'
gem 'httparty', '~> 0.18.1' gem 'httparty', '~> 0.18.1'
gem 'active_storage_validations', '~> 0.9.5'
\ No newline at end of file
...@@ -56,6 +56,8 @@ GEM ...@@ -56,6 +56,8 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_storage_validations (0.9.5)
rails (>= 5.2.0)
activejob (6.1.4) activejob (6.1.4)
activesupport (= 6.1.4) activesupport (= 6.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
...@@ -265,6 +267,7 @@ PLATFORMS ...@@ -265,6 +267,7 @@ PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
active_storage_validations (~> 0.9.5)
bootsnap (>= 1.4.4) bootsnap (>= 1.4.4)
bullet (~> 6.1, >= 6.1.4) bullet (~> 6.1, >= 6.1.4)
byebug byebug
......
.ribbon {
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;
}
}
class AppliesController < ApplicationController
before_action :load_job, only: %i[new confirm create]
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
def confirm
@apply = current_user.apply_jobs.build(apply_params)
@apply.job = @job
if @apply.valid?
@blob = ActiveStorage::Blob.create_and_upload!(
io: apply_params[:cv],
filename: apply_params[:cv].original_filename,
content_type: apply_params[:cv].content_type
)
session[:blob_id] = @blob.id
session[:apply_job] = @apply
else
render 'new'
end
end
def create
@apply = current_user.apply_jobs.build(apply_params)
@apply.job = @job
@apply.cv = ActiveStorage::Blob.find(session[:blob_id])
if @apply.save
UserMailer.apply_job(current_user, @job, @apply).deliver_now
flash.now[:info] = 'Job application information has been sent to your email'
else
render 'new'
end
session[:blob_id] = nil
session[:apply_job] = nil
end
private
def load_job
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
def apply_params
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
...@@ -8,7 +8,8 @@ class JobsController < ApplicationController ...@@ -8,7 +8,8 @@ class JobsController < ApplicationController
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 end
private private
......
module AppliesHelper
end
...@@ -15,3 +15,4 @@ ActiveStorage.start() ...@@ -15,3 +15,4 @@ ActiveStorage.start()
import "bootstrap" import "bootstrap"
window.bootstrap = require("bootstrap"); window.bootstrap = require("bootstrap");
import "../stylesheets/application.scss"; import "../stylesheets/application.scss";
import "./validation";
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 UserMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.apply_job.subject
#
def apply_job(user, job, apply)
@user = user
@job = job
@apply = apply
mail to: user.email, subject: 'aplly job'
end
end
class ApplyJob < ApplicationRecord class ApplyJob < ApplicationRecord
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
has_one_attached :cv has_one_attached :cv
validates :user_name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
validates :cv, presence: true, content_type:
{ in: ACCEPT_CONTENT_TYPE,
message: 'must be a valid cv format' },
size:
{ less_than: 5.megabytes,
message: 'should be less than 5MB' }
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, 'Confirmation')
.container
= render 'ribbon'
.container
h1.my-5.text-center
| Confirmation
.col
= form_with(model: @apply, scope: :apply_job, url: done_job_path, local: true) do |f|
.row.mb-5
= f.hidden_field :job_id, value: @job.id
= f.hidden_field :blob_id, value: @blob.id
.col-2
= f.label :name, 'Full name:', class: 'form-label label'
.col-10
span.mx-2.label.form-control
= @apply.user_name
= f.hidden_field :user_name, value: @apply.user_name
.row.mb-5
.col-2
= f.label :email, 'Email:', class: 'form-label label'
.col-10
span.mx-2.label.form-control
= @apply.email
= f.hidden_field :email, value: @apply.email
.row.mb-5
.col-2
= 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
h3.my-4
| You have successfully applied for the #{@job.title}
p Good luck! Thank you for using our service
= link_to 'Keep looking for another job', root_path
- provide(:title, 'Apply job')
.container
= render 'ribbon'
.container
h1.my-5.text-center
| Apply Form
p.fs-5.fw-bold.mb-4.text-center
= @job.title
.col
= form_with(model: @apply, url: confirm_job_path, local: true, id: 'apply-form') do |f|
= render 'shared/error_messages', object: f.object
.row.mb-5.form-group
= f.hidden_field :job_id, value: @job.id
.col-2
= f.label :user_name, 'Full name', class: 'form-label label'
.col-10
= f.text_field :user_name, value: @apply.user_name || @current_user.name, class: 'form-control'
span.form-msg
.row.mb-5.form-group
.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')
]
})
...@@ -22,7 +22,7 @@ ruby: ...@@ -22,7 +22,7 @@ ruby:
.job-apply.d-flex.align-items-center.justify-content-between .job-apply.d-flex.align-items-center.justify-content-between
h2.align-items-start h2.align-items-start
= @job.title = @job.title
= link_to 'Apply for this job', '#', class: 'btn btn-primary' = link_to 'Apply for this job', apply_job_path(job_id: @job.id), class: 'btn btn-primary'
p.text-secondary p.text-secondary
= @job.company.name = @job.company.name
.row.bg-light .row.bg-light
...@@ -84,5 +84,5 @@ ruby: ...@@ -84,5 +84,5 @@ ruby:
li.text-secondary li.text-secondary
= info = info
.job-apply.d-flex.align-items-center.justify-content-between .job-apply.d-flex.align-items-center.justify-content-between
= link_to 'Apply for this job', '#', class: 'btn btn-primary' = link_to 'Apply for this job', apply_job_path(job_id: @job.id), class: 'btn btn-primary'
= link_to 'Favorite', '#', class: 'btn btn-primary' = link_to 'Favorite', '#', class: 'btn btn-primary'
...@@ -11,6 +11,8 @@ html ...@@ -11,6 +11,8 @@ html
= render 'layouts/shim' = render 'layouts/shim'
body body
= render 'layouts/header' = render 'layouts/header'
.container-fluid .container-fluid style ="min-height:80vh;"
- flash.each do |message_type, message|
div class=("text-center alert alert-#{message_type}") = message
= yield = yield
= render 'layouts/footer' = render 'layouts/footer'
\ No newline at end of file
- if object.errors.any?
#error_explanation.bg_danger
.alert.alert-danger
| The form contains
= pluralize(object.errors.count, "error")
ul.mb-3
- object.errors.full_messages.each do |msg|
li.text-danger
= 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
h1 VenJob
p
| Dear #{@user.name},
p Thank you for applied with VenJOB. Your applied job's information is as follow:
p
| Job title: #{@job.title}
p
| Location:
= show_location(@job.cities)
p
| Company: #{@job.company.name}
br/
p Your submitted information:
p
| Full name: #{@apply.user_name}
p
| Email: #{@apply.email}
p
| CV: #{url_for(@apply.cv)}
p Best,
' VenJob
= "\r\n" * 2
' Dear #{@user.name},
= "\n"
' Thank you for applied with VenJOB. Your applied job's information is as follow:
= "\n"
' Job title: #{@job.title}
= "\n"
' Location: #{show_location(@job.cities)}
= "\n"
' Company: #{@job.company.name}
= "\n"
' Your submitted information:
= "\n"
' Full name: #{@apply.user_name}
= "\n"
' Email: #{@apply.email}
= "\n"
' CV: #{url_for(@apply.cv)}
= "\n"
' 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
......
...@@ -81,6 +81,13 @@ Rails.application.configure do ...@@ -81,6 +81,13 @@ Rails.application.configure do
# Bullet.raise = true # raise an error if n+1 query occurs # Bullet.raise = true # raise an error if n+1 query occurs
# Bullet.unused_eager_loading_enable = false # Bullet.unused_eager_loading_enable = false
# end # end
config.action_mailer.raise_delivery_errors = false
host = 'localhost:3000' # Local
# config.action_mailer.default_url_options = { host: host, protocol: 'https' }
# Use this if developing on localhost.
config.action_mailer.default_url_options = { host: host, protocol: 'http' }
config.after_initialize do config.after_initialize do
Bullet.enable = true Bullet.enable = true
Bullet.alert = true Bullet.alert = true
...@@ -89,4 +96,8 @@ Rails.application.configure do ...@@ -89,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
...@@ -6,4 +6,8 @@ Rails.application.routes.draw do ...@@ -6,4 +6,8 @@ Rails.application.routes.draw do
get '/jobs/:model/:slug', to: 'jobs#index', as: :job_list get '/jobs/:model/:slug', to: 'jobs#index', as: :job_list
resources :jobs, only: %i[index show] resources :jobs, only: %i[index show]
get '/apply', to: 'applies#new', as: :apply_job
post '/confirm', to: 'applies#confirm', as: :confirm_job
post '/done', to: 'applies#create', as: :done_job
end end
class AddNameAndEmailToApplyJobs < ActiveRecord::Migration[6.1]
def change
add_column :apply_jobs, :user_name, :string
add_column :apply_jobs, :email, :string
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_07_22_080023) do ActiveRecord::Schema.define(version: 2021_07_29_140527) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
...@@ -45,6 +45,8 @@ ActiveRecord::Schema.define(version: 2021_07_22_080023) do ...@@ -45,6 +45,8 @@ ActiveRecord::Schema.define(version: 2021_07_22_080023) do
t.bigint "user_id", null: false t.bigint "user_id", null: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.string "user_name"
t.string "email"
t.index ["job_id"], name: "index_apply_jobs_on_job_id" t.index ["job_id"], name: "index_apply_jobs_on_job_id"
t.index ["user_id"], name: "index_apply_jobs_on_user_id" t.index ["user_id"], name: "index_apply_jobs_on_user_id"
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",
......
require "test_helper"
class AppliesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/apply_job
def apply_job
UserMailer.apply_job
end
end
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "apply_job" do
mail = UserMailer.apply_job
assert_equal "Apply job", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
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