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

  1. Add
    has_many :work_schedule_templates, \
    as: :templateable, dependent: :destroy
    to the team class.
  2. 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

Walther H Diechmann

Walther H Diechmann

Got on the train just about when the IBM PC 5150 got introduced and never really got off again - switched to macOS about 2006 though, and never looked back. It's been such a ride!
Silkeborg, Denmark