Skip to content

Commit

Permalink
Merge pull request #155 from Ches-ctrl/applying-validations
Browse files Browse the repository at this point in the history
Added some simple validations for the application form as well as a t…
  • Loading branch information
Ches-ctrl authored May 24, 2024
2 parents b1ff6e6 + 66c6f02 commit 9fa3aa9
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 36 deletions.
4 changes: 4 additions & 0 deletions app/assets/stylesheets/components/_button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
padding: 10px 20px;
text-decoration: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
&:hover {
opacity: 85%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
display: flex;
position: relative;
z-index: 10;
align-items: center;

.toggle-reveal-application-details {
all: unset;
Expand Down
9 changes: 7 additions & 2 deletions app/controllers/job_applications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class JobApplicationsController < ApplicationController
# TODO: Fix issue with cookies where they are not removed when the user goes back to job_applications/new

def index
@job_applications = JobApplication.all.where(user_id: current_user.id)
@job_applications = JobApplication.all.where(user_id: current_user.id, status: "Applied" || "Application pending")
@failed_applications = JobApplication.all.where(user_id: current_user.id, status: "Submission failed")
end

def show
Expand Down Expand Up @@ -65,8 +66,8 @@ def create
end

def process_application(job_application)
ApplyJob.perform_later(job_application.id, current_user.id)
job_application.update(status: "Application pending")
ApplyJob.perform_later(job_application.id, current_user.id)
user_saved_jobs = SavedJob.where(user_id: current_user.id)
user_saved_jobs.find_by(job_id: job_application.job.id).destroy
ActionCable.server.broadcast("job_applications_#{current_user.id}", {
Expand Down Expand Up @@ -94,6 +95,10 @@ def process_application(job_application)
def success
end

def manual_submission
@url = params[:url]
end

private

# TODO: Update job_application_params to include the user inputs
Expand Down
72 changes: 65 additions & 7 deletions app/javascript/controllers/all_application_forms_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class extends Controller {
this.consumer = createConsumer();
this.jobsCount = parseInt(this.data.get("jobsCount"), 10);
this.appliedJobCount = 0;
this.failedCount = 0;
this.createWebSocketChannel(this.userValue);
}

Expand All @@ -30,6 +31,12 @@ export default class extends Controller {

async submitAllForms(event) {
event.preventDefault();
const isValid = this.validateForms();

if ( !isValid ) {
return;
}

this.buttonTarget.disabled = true;
this.overlayTarget.classList.remove("d-none");

Expand All @@ -40,10 +47,48 @@ export default class extends Controller {
return fetch(form.action, {
method: 'POST',
body: formData
});
}).then(response => response.json())
.then(data => {
if ( !response.ok) {
this.handleServerSideErrors(form, data.errors);
}
})
});
}

validateForms() {
let firstInvalidField = null;

this.formTargets.forEach((form) => {
if ( !form.checkValidity()) {
const invalidFields = form.querySelectorAll(":invalid");
invalidFields.forEach(field => {
field.classList.add("is-invalid");
if ( !firstInvalidField) {
firstInvalidField = field;
}
});
}
});

if (firstInvalidField) {
firstInvalidField.focus();
firstInvalidField.scrollIntoView({ behavior: "smooth", block: "center" });
return false
}

return true;
}

handleServerSideErrors(form, errors) {
for (const [field, messages] of Object.entries(errors)) {
const input = form.querySelector(`[name="${field}"]`);
const errorElement = input.nextElementSibling;
errorElement.textContent = messages.join(", ");
input.classList.add("is-invalid");
}
}



createWebSocketChannel(userValue) {
Expand Down Expand Up @@ -71,20 +116,29 @@ export default class extends Controller {

const spinner = document.querySelector(`[data-all-application-forms-id="${jobId}"]`);
const checkmark = document.querySelector(`[data-all-application-forms-id="${jobId}-success"]`);
const crossmark = document.querySelector(`[data-all-application-forms-id="${jobId}-failed"]`);

if (spinner && checkmark) {
if (spinner && checkmark && crossmark) {
if (status === "Applied") {
spinner.classList.add("d-none");
checkmark.classList.remove("d-none");

this.appliedJobCount++;

if (this.appliedJobCount === this.jobsCount) {
this.redirectToSuccessPage();
window.location.href = "/job_applications/success"; // this line redundant?
} else if (status === "Submission failed") {
spinner.classList.add("d-none");
crossmark.classList.remove("d-none");

this.failedCount++;
}

if (this.appliedJobCount + this.failedCount === this.jobsCount) {
if (this.failedCount > 0) {
alert("Some applications failed to submit. Please submit them manually.");
this.redirectToApplicationsPage();
} else {
this.redirectToSuccessPage();s
}
} else {
// Handle other statuses if needed
}
}
}
Expand All @@ -97,6 +151,10 @@ export default class extends Controller {
window.location.href = "/job_applications/success";
}

redirectToApplicationsPage() {
window.location.href = "/job_applications";
}

disconnect() {
console.log("Disconnecting from JobApplicationsChannel");
this.channel.unsubscribe();
Expand Down
13 changes: 8 additions & 5 deletions app/jobs/apply_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ class ApplyJob < ApplicationJob
queue_as :default
sidekiq_options retry: false

# TODO: Tidy up (1) ApplyJob (2) FormFiller and (3) SubmitApplication so that the correct functionality sits in each class/module

def perform(job_application_id, user_id)
p "Hello from the job application job!"
application = JobApplication.find(job_application_id)
Expand All @@ -15,19 +17,20 @@ def perform(job_application_id, user_id)
fields_to_fill = application_criteria

form_filler = FormFiller.new(job.posting_url, fields_to_fill, job_application_id)
form_filler.fill_out_form
result = form_filler.fill_out_form

user_channel_name = "job_applications_#{user.id}"

status = result ? "Applied" : "Submission Failed"

ActionCable.server.broadcast(
user_channel_name,
{
event: "job-application-submitted",
event: "job_application_submitted",
job_application_id: application.id,
user_id: application.user_id,
user_id: application.user.id,
job_id: job.id,
status: "Applied"
# Include any additional data you want to send to the frontend
status:
}
)
end
Expand Down
47 changes: 35 additions & 12 deletions app/models/concerns/ats/greenhouse/submit_application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,43 @@ def submit_application
session.visit(@url)
p "Successfully reached #{@url}"
find_apply_button(session).click

@fields.each do |field|
field = field[1]
handle_field_interaction(session, field)
end
take_screenshot_and_store(session)

find_submit_button(session).click

if session.current_url.include?('confirmation')
success = true
@job_application.update(status: 'Applied')
p "Form submitted successfully"
else
success = false
@job_application.update(status: 'Submission failed')
p "Form submission failed"
end

@fields.each do |field|
field[1]
if field[0] == 'resume'
file_path = Rails.root.join('tmp', "Resume - #{@user.first_name} #{@user.last_name} - - #{@job.title} - #{@job.company.name}.pdf")
FileUtils.rm_f(file_path)
elsif field[0] == 'cover_letter_'
file_path = Rails.root.join('tmp',
"Cover Letter - #{@job.title} - #{@job.company.name} - #{@user.first_name} #{@user.last_name}.docx")
file_path = Rails.root.join('tmp', "Cover Letter - #{@job.title} - #{@job.company.name} - #{@user.first_name} #{@user.last_name}.docx")
FileUtils.rm_f(file_path)
end
end
rescue StandardError
nil
rescue StandardError => e
p "Error: #{e}"
success = false
@job_application.update(status: 'Submission failed')
ensure
take_screenshot_and_store(session, job_application_id)
session.driver.quit
end
success
end

def handle_field_interaction(session, field)
Expand Down Expand Up @@ -113,7 +129,7 @@ def select_option_from_select(session, listbox_locator, option_locator, option_t
session.within "##{listbox_locator}" do
session.find(option_locator, text: option_text).click
end
rescue Selenium::WebDriver::Error::ElementNotInteractableError
rescue Selenium::WebDriver::Error::ElementNotInteractableError, Selenium::WebDriver::Error::JavascriptError
new_locator = session.find("label ##{listbox_locator}")
new_locator.ancestor("label").find("a").click
session.find("li", text: option_text).click
Expand All @@ -139,17 +155,25 @@ def select_options_from_checkbox(session, checkbox_locator, option_text)
end
end

# rubocop:disable Security/Open
def upload_file(session, upload_locator, file)
# NB. Changed this from previous URI.open due to security issue - noting in case this breaks functionality (CC)
if file.instance_of?(String)
docx = Htmltoword::Document.create(file)
file_path = Rails.root.join('tmp',
"Cover Letter - #{@job.title} - #{@job.company.name} - #{@user.first_name} #{@user.last_name}.docx")
file_path = Rails.root.join("tmp", "Cover Letter - #{@job.title} - #{@job.company.name} - #{@user.first_name} #{@user.last_name}.docx")
File.binwrite(file_path, docx)
else
file_path = Rails.root.join('tmp', "Resume - #{@user.first_name} #{@user.last_name} - #{@job.title} - #{@job.company.name}.pdf")
File.binwrite(file_path, URI.open(file.url).read)
uri = URI.parse(file.url)

raise "Invalid URL scheme" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)

response = Net::HTTP.get_response(uri)

raise "Failed to download file: #{response.message}" unless response.is_a?(Net::HTTPSuccess)

file_path = Rails.root.join("tmp", "Resume - #{@user.first_name} #{@user.last_name} - #{@job.title} - #{@job.company.name}.pdf")
File.binwrite(file_path, response.body)
end

begin
session.find(upload_locator).attach_file(file_path)
rescue Capybara::ElementNotFound
Expand All @@ -158,7 +182,6 @@ def upload_file(session, upload_locator, file)
end
end
end
# rubocop:enable Security/Open
end
end
end
8 changes: 0 additions & 8 deletions app/services/form_filler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ class FormFiller

# TODO: Handle job posting becoming closed (redirect or notification on page)
# TODO: Review code for inefficient loops and potential optimisations
# TODO: Add ruby monitoring tools to monitor performance and execution
# TODO: Implement caching for both user and form inputs. At the moment we request the database every time we want an input
# TODO: Cache values at beginning of session and then update cache when user changes values
# TODO: Enable multi-job application support in form_filler and cache before all applications are submitted
# TODO: Restrict search to certain portions of the page

# Could we implement caching for form inputs? So once you've done it once it becomes less intensive

def initialize(url, fields, job_application_id)
@url = url
@fields = fields
Expand All @@ -31,11 +28,6 @@ def initialize(url, fields, job_application_id)

def fill_out_form
submit_application

# TODO: Add check on whether form has been submitted successfully
# submit = find_submit_button.click rescue nil

@job_application.update(status: 'Applied')
end

private
Expand Down
1 change: 1 addition & 0 deletions app/views/job_applications/_loading.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<span class="visually-hidden">Loading...</span>
</div>
<i data-all-application-forms-id="<%= job.id %>-success" class="fa-solid fa-check d-none" style="color: #7eed8b;"></i>
<i data-all-application-forms-id="<%= job.id %>-failed" class="fa-solid fa-xmark d-none" style="color: red;"></i>
</div>
</div>
<% end %>
Expand Down
29 changes: 29 additions & 0 deletions app/views/job_applications/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
<div class="container">
<% if current_user.job_applications.where(status: "Submission failed") %>
<h3>Manual Submission required</h3>
<% @failed_applications.each do |failed_application| %>
<div class='application-wrapper' data-controller="reveal-myapplication-details">
<div class="application-main-info">
<button class="toggle-reveal-application-details" data-reveal-myapplication-details-target="toggleReveal" data-action="click->reveal-myapplication-details#revealApplicationDetails">
<i class="fa-solid fa-caret-right"></i>
</button>
<h5 class="m-0"><%= failed_application.job.title %> @
<%= failed_application.job.company.name %> </h5> -
<%= failed_application.status %> -
<%= link_to "Apply Manually", failed_application.job.posting_url, target: "_blank", class: "btn-apply" %>
</div>

<div id="<%= failed_application.id %>" class="application-secondary-info" data-reveal-myapplication-details-target="applicationDetails">

<div class="application-secondary-info-left">
<p><%= failed_application.job.description %></p>
<div>
<span class="meta-info">Applications Close: <%= failed_application.job.deadline.strftime('%d %B %Y') if failed_application.job.deadline %> - </span>
<span class="meta-info">Date Applied: <%= failed_application.updated_at.strftime('%d %B %Y') %></span>
</div>
<hr class="d-none">
</div>
</div>
</div>
<% end %>
<% end %>

<h3 class="my-applications">My Applications</h3>
<% @job_applications.each do |application| %>
<div class='application-wrapper' data-controller="reveal-myapplication-details">
Expand Down
3 changes: 2 additions & 1 deletion app/views/jobs/_job_card.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
<%= render partial: 'jobs/basic_info', locals: { job: job } %>
</div>
<div class="d-flex flex-row justify-content-between align-items-center w-100">
<% include_checkboxes ||= false %>
<%= render partial: 'jobs/climate_rating', locals: { job: job } %>
<%= render partial: 'jobs/difficulty_rating', locals: { job: job } %>
<%= render partial: 'jobs/add_job_to_basket', locals: { job: job, saved_job_ids: @saved_job_ids } %>
<%= render partial: 'jobs/add_job_to_basket', locals: { job: job, saved_job_ids: @saved_job_ids, include_checkboxes: include_checkboxes } %>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
<div class="semi-donut" style="--percentage: 50; --fill:#FFC300">Pending</div>
<% elsif application.status == "Applied" %>
<div class="semi-donut" style="--percentage: 100; --fill:#6007C6">Applied</div>
<% elsif application.status == "Submission failed" %>
<%= link_to "Apply Manually", application.job.posting_url, target: "_blank", class: "btn-apply" %>
<% end %>

</div>
Expand Down
2 changes: 1 addition & 1 deletion config/sidekiq.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:concurrency: 5
:concurrency: 1
:timeout: 60
:verbose: true
:queues:
Expand Down

0 comments on commit 9fa3aa9

Please sign in to comment.