Building Mortimer, day 3
I'm not sure whether I am fighting the clock here – it somehow feels like it, anyways, here we go again! If you lust there are things that happened yesterday on day 2, or you could skip forward, there is always a chance to learn about the beginning up until this moment, and then of cause there is Vol 4 reboot, day 1.
Day 3
It cannot continue, this cheating with chronology – but I have to put down one/two more events even if this is now technically day 3 and I should be off to my bed hours ago.
BTW gave me self a small treat: I binged Wyatt Earp and the Cowboy War which certainly is binge-worthy!
WorkScheduleTemplates
There are not that many use cases left but this one actually is a rather interesting on at it:
as a user I can assign working hours to a work schedule template – and on workdays where I work as planned I will not have to punch in/out, an automat will do the punching according to my work schedule template
First I needed somewhere to persist the schedule
rails g scaffold WorkScheduleTemplate \
name templateable:references{polymorphic} \
start_at:datetime stop_at:datetime week_schedule
Then I had to use a "Rails special" (I know it's not a particular Rails special per se but humor me here): the polymorphic association and I'll disclose the reason later, but trust me there is a valid argument!
After migrating the DB we tell the user that they may
has_many :work_schedule_templates, as: :templateable, dependent: :destroy
Then we are ready to play with the templateable thing in the console
mortimer(dev)* User.first.work_schedule_templates <<
mortimer(dev)> WorkScheduleTemplate.new( name: 'test')
mortimer(dev)> User.first.work_schedule_templates.count
=> 1
Getting to the form always drives me crazy - the select I mean. With work schedule templates being able to attach to any user, we need to be able to select a user. Here is a nice post on f.collection_select
<div>
<%= form.label :templateable, style: "display: block" %>
<%= form.text_field :templateable_type, value: 'User' %>
<%= form.collection_select :templateable_id, User.all, :id, :email %>
</div>
That was the first half of the use case down.
Background job
The second half is a background job that will require some serious mastication including controlling the job, setting up the process to run (the actual automate), and the logic to decide when to actually punch.
The actual automate was not too much of a challenge really
rails g job AutomatePunching
prepared the necessary files for me, and then I added the test to make green
require "test_helper"
class AutomatePunchingJobTest < ActiveJob::TestCase
test "should create a punch if no punches today" do
assert_difference("Punch.count") do
AutomatePunchingJob.perform_now
end
end
end
Now it is time for me to disclose why I went with the polymorphic association above! It's all about this use case (from day 2)
as a user I may be assigned to a team
and to operate efficiently an admin user would have this use case
as an admin user I will assign a work_schedule_template to a team in effect covering the work schedule for all team members
Building on the polymorphic properties of the work_schedule_template it was real easy to fix this use case
- Add
has_many :work_schedule_templates, \
to the team class.
as: :templateable, dependent: :destroy - Add a second select to the work schedule template form like this
<div>
<%= form.label :templateable, style: "display: block" %>
<%= form.text_field :templateable_type, value: 'User' %>
<%= form.collection_select :templateable_id, User.all, :id, :email, { include_blank: "Select User or Team"} %>
</div>
<div>
<%= form.label :templateable, style: "display: block" %>
<%= form.text_field :templateable_type, value: 'Team' %>
<%= form.collection_select :templateable_id, Team.all, :id, :email, { include_blank: "Select User or Team"} %>
</div>
(and I know – this can be greatly improved UX-wise – but I will leave that for a future complication.
With that off my chest we can return to the automate half of todays use case – remember?
...an automat will do the punching according to my work schedule template
So, I have to collect all work schedule templates for a user – theirs or those of their team. The naiive implementation looks like this in the console
If a user has indeed a work schedule template then we need to know if it spans 'today'
class WorkScheduleTemplate < ApplicationRecord
belongs_to :templateable, polymorphic: true
scope :spanning_today, -> { where("start_at <= ? AND stop_at >= ?", Time.now.utc, Time.now.utc) }
end
What's with the UTC you might ask? Well, Mortimer aspires to solve the Time & Attendance issue across time zones hence all punches and 'clock' values are persisted in UTC and then transposed into the users timezone on its way to the UI; convoluted I know but there is not a lot to do about it (in fact there is but world leaders are not ready to take the plunge, perhaps remembering how the last attempt fared). Anyways, I digress –
This is part of the secret sauce – so I'd better double down on testing it!
require "test_helper"
class AutomatePunchingJobTest < ActiveJob::TestCase
test "no need for punches user has punches today" do
assert_difference("Punch.count", 0) do
AutomatePunchingJob.perform_now(users: [ users(:one) ])
end
end
test "no need for punches user has no schedule for today" do
assert_difference("Punch.count", 0) do
AutomatePunchingJob.perform_now(users: [ users(:one) ], day: Time.now-3.days)
end
end
test "should create 2 punches - user has no punches today" do
assert_difference("Punch.count", 2) do
AutomatePunchingJob.perform_now(users: [ users(:two) ])
end
end
test "should create 1 punch - user has punched in today" do
assert_difference("Punch.count", 1) do
AutomatePunchingJob.perform_now(users: [ users(:three) ])
end
end
end
Making this job run every night at 23:55 is a job for Solid Queue.
Solid Queue
First I need to install the thing with bundle add solid_queue
so I trawled the README and this post. I opted for the Puma plugin option.
Then I added the AutomatePunchingJob (recurring) job to the config/recurring.yml
automate_punching_every_night:
class: AutomatePunchingJob
queue: background
schedule: every 5 minutes
and that was that – I commit'ed and tagged it 0.3.0_work_schedule_templates