Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[plugin] timesheet tracker (#1313) #1314

Merged
merged 1 commit into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions test/fixtures/api_actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,89 @@ create_api_action_plugin_bishop_monitoring_email:
include_api_resource_data: true
email: [email protected]
api_namespace: monitoring_target_incident

custom_api_action_on_timesheet_request:
type: CreateApiAction
action_type: custom_action
include_api_resource_data: true
payload_mapping:
api_namespace: timesheet_request
method_definition: |
# get time tracker namespace
time_tracker = ApiNamespace.find_by(name: 'time_tracker')

format = "%Y-%m-%d"
if api_resource.properties['current_month'] == "true"
api_resource.update(properties: api_resource.properties.merge({ start_date: Date.today.beginning_of_month.strftime(format), end_date: Date.today.end_of_month.strftime(format) }))
end

# get time tracker entries for the current user within the specified time
start_date = Date.parse(api_resource.properties['start_date'])
end_date = Date.parse(api_resource.properties['end_date'])
email = current_user.email

current_user_time_tracker_entries = ApiResource.where([
"api_namespace_id = :api_namespace_id
and created_at::date >= :start_date
and created_at::date <= :end_date
and properties->>'email_address' = :email",
{
api_namespace_id: time_tracker.id,
start_date: start_date,
end_date: end_date,
email: email
}
])

# add email of user requesting timesheet
api_resource.update(properties: api_resource.properties.merge({ requested_by: email }))

# get the consultant rate and check if they are active
consultant = ApiNamespace.includes(:api_resources).find_by(name: 'consultant').api_resources.find { |consultant| consultant.properties['email'] == email }

send_email = ->(email, subject, content, attachment) {
email_thread = MessageThread.create(recipients: [email], subject: subject)
email_message = email_thread.messages.create(content: content, from: "noreply@#{ENV["APP_HOST"]}")
attachments = !attachment ? [] : [attachment]

EMailer.with(message: email_message, message_thread: email_thread, attachments: attachments).ship.deliver_later
}

if consultant.nil?
send_email.(email, "Unable to generate timesheet", "Please register as a consultant and add your work hours to request timesheet.", nil)
return
end

if consultant.properties['active'].to_s.downcase == "false"
send_email.(email, "Unable to generate timesheet", "Inactive consultants cannot request timesheet. Please contact administrator.", nil)
return
end

rate = Float(consultant.properties['rate'].to_s, exception: false)
if rate == nil || rate <= 0
send_email.(email, "Unable to generate timesheet", "Invalid hourly rate for consultant.", nil)
return
end

total_hours = current_user_time_tracker_entries.sum { |entry| entry.properties['how_much_time_in_hours_spent'].to_d }

# construct the csv file
csv_string = CSV.generate do |csv|
csv << ['', 'Rate', rate]
csv << ['', 'Total Hours', total_hours]
csv << ['', 'Grand Total', total_hours * rate]
csv << ['', '', '']
csv << ['Date', 'Hours', 'User', 'Client', 'Task', 'Notes']
current_user_time_tracker_entries.map { |entry| csv << [entry.created_at.to_s, entry.properties['how_much_time_in_hours_spent'], entry.properties['email_address'], entry.properties['for_what_client'], entry.properties['what_task_did_you_work_on'], entry.properties['notes']] }
end

# email the created csv file as an attachment
email_content = csv_string.html_safe
subject = "Timesheet for #{email} from #{api_resource.properties['start_date']} to #{api_resource.properties['end_date']}"
attachment = { filename: "#{subject}.csv", mime_type: "text/csv", content: email_content }
content = "Please find your timesheet attached to this email "

blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(attachment[:content]), filename: attachment[:filename], content_type: attachment[:mime_type], metadata: nil)
content += ActionText::Content.new("<action-text-attachment sgid='#{blob.attachable_sgid}'></action-text-attachment>").to_s

send_email.(email, subject, content, attachment)
25 changes: 25 additions & 0 deletions test/fixtures/api_namespaces.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,28 @@ namespace_with_transcript:
"transcript": "",
"transcript_parsed": false
}

time_tracker:
name: time_tracker
slug: time-tracker
version: 1
properties:
{
"notes": "",
"for_what_client": "",
"what_task_did_you_work_on": "",
"how_much_time_in_hours_spent": 0,
"i_certify_information_accurate": false,
}

consultant:
name: consultant
slug: consultant
version: 1
properties: { "active": false, "name": "", "rate": 0, "email": "" }

timesheet_request:
name: consultant/timesheet_request
slug: consultant-timesheet_request
version: 1
properties: { "start_date": "", "end_date": "", "current_month": true }
60 changes: 60 additions & 0 deletions test/fixtures/api_resources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,63 @@ transcript:
"transcript": "",
"transcript_parsed": false
}

consultant_one:
api_namespace: consultant
properties:
{
"active": true,
"name": "Test User",
"rate": 20.0,
"email": "[email protected]",
}

tracker_entry_one:
api_namespace: time_tracker
properties:
{
"notes": "http://link-to-backlog-task-1",
"for_what_client": "Retarone Solutions",
"what_task_did_you_work_on": "Setup 2FA",
"how_much_time_in_hours_spent": 7.3,
"i_certify_information_accurate": true,
"email_address": "[email protected]",
}

previous_month_tracker_entry_two:
api_namespace: time_tracker
created_at: <%= 1.month.ago %>
properties:
{
"notes": "http://link-to-backlog-task-2",
"for_what_client": "Retarone Solutions",
"what_task_did_you_work_on": "Add proper form validations in user login",
"how_much_time_in_hours_spent": 4.5,
"i_certify_information_accurate": true,
"email_address": "[email protected]",
}

tracker_entry_three:
api_namespace: time_tracker
properties:
{
"notes": "http://link-to-backlog-task-3",
"for_what_client": "Retarone Solutions",
"what_task_did_you_work_on": "Setup CI/CD with GitLab",
"how_much_time_in_hours_spent": 3.5,
"i_certify_information_accurate": true,
"email_address": "[email protected]",
}

timesheet_request_date_range_previous_month:
api_namespace: timesheet_request
properties:
{
"start_date": <%= 1.month.ago.beginning_of_month %>,
"end_date": <%= 1.month.ago.end_of_month %>,
"current_month": "false",
}

timesheet_request_current_month:
api_namespace: timesheet_request
properties: { "start_date": "", "end_date": "", "current_month": "true" }
152 changes: 152 additions & 0 deletions test/plugins/timesheet_request_plugin_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
require "test_helper"

class TimesheetRequestPluginTest < ActiveSupport::TestCase
setup do
@consultant = api_resources(:consultant_one)
@timesheet_request = api_namespaces(:timesheet_request)
@api_action = api_actions(:custom_api_action_on_timesheet_request)

@api_resource_date_range = api_resources(:timesheet_request_date_range_previous_month)
@api_resource_current_month = api_resources(:timesheet_request_current_month)

# Setting current_user for custom_api_action
Current.user = users(:one) # [email protected]

Sidekiq::Testing::fake!
end

test "Should send email on requesting timesheet" do
@api_action.update({
api_resource_id: @api_resource_current_month.id
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last

assert_equal 1, sent_email.attachments.length
end

test "Should get all the hours for current month" do
@api_action.update({
api_resource_id: @api_resource_current_month.id
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last
csv_file = sent_email.attachments[0]

parsed_csv = CSV.parse(csv_file.body.raw_source.gsub(/\r\n/, "$"), col_sep: ",", row_sep: "$")

rate = @consultant.properties["rate"]
assert_equal rate, parsed_csv[0][2].to_d

entries = [
api_resources(:tracker_entry_one),
api_resources(:tracker_entry_three)
]

csv_tasks = []
entries.each { | entry | csv_tasks.push([
entry.created_at.to_s,
entry.properties["how_much_time_in_hours_spent"].to_s,
entry.properties["email_address"],
entry.properties["for_what_client"],
entry.properties["what_task_did_you_work_on"],
entry.properties["notes"]
]) }

csv_tasks.each { | task | assert parsed_csv.include?(task) }

grand_total = 0
entries.each { | entry | grand_total += entry.properties["how_much_time_in_hours_spent"]}
grand_total *= rate
assert_equal grand_total, parsed_csv[2][2].to_d
end

test "Should get all the hours for specified time period" do
@api_action.update!({
api_resource_id: @api_resource_date_range.id,
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last
csv_file = sent_email.attachments[0]

parsed_csv = CSV.parse(csv_file.body.raw_source.gsub(/\r\n/, "$"), col_sep: ",", row_sep: "$")
rate = @consultant.properties["rate"]
assert_equal rate, parsed_csv[0][2].to_d

entry_one = api_resources(:previous_month_tracker_entry_two)
assert_equal entry_one.properties["how_much_time_in_hours_spent"], parsed_csv[1][2].to_d
assert_equal entry_one.properties["for_what_client"], parsed_csv[5][3]
assert_equal entry_one.properties["notes"], parsed_csv[5][5]

grand_total = entry_one.properties["how_much_time_in_hours_spent"] * rate
assert_equal grand_total, parsed_csv[2][2].to_d
end

test "Should send an email without timesheet for inactive consultants" do
@consultant.update({
properties: @consultant.properties.merge({ active: false })
})

@api_action.update!({
api_resource_id: @api_resource_date_range.id,
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last
assert_equal 0, sent_email.attachments.length
assert_equal "Unable to generate timesheet", sent_email.subject
assert sent_email.body.include?("Inactive consultants cannot request timesheet")
end

test "Should send an email without timesheet if consultant's rate is invalid" do
@consultant.update({
properties: @consultant.properties.merge({ rate: "20.0$" })
})

@api_action.update!({
api_resource_id: @api_resource_date_range.id,
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last
assert_equal 0, sent_email.attachments.length
assert_equal "Unable to generate timesheet", sent_email.subject
assert sent_email.body.include?("Invalid hourly rate for consultant")
end


test "Should send an email without timesheet for non existing consultants" do
@consultant.delete

@api_action.update!({
api_resource_id: @api_resource_date_range.id,
})

perform_enqueued_jobs do
@api_action.execute_action
end

sent_email = ActionMailer::Base.deliveries.last
assert_equal 0, sent_email.attachments.length
assert_equal "Unable to generate timesheet", sent_email.subject
assert sent_email.body.include?("Please register as a consultant")
end
end