Creating a monthly updating calendar in Rails with Whenever gem and a Cron job
Tags: rails, cron, whenever, calendar
Published: 2021-08-02 17:36:41

For my final portfolio project with the Flatiron School, I updated an app I'd previously created with Sinatra into React and also added additional functionality. This app allows a user to track plant care in their home, particularly watering needs for their plants. The user saw a calendar (created with React Calendar) that rendered a watering schedule on the front end. Calendar data was maintained on the back end with a `PlantEvent` model and database. To limit the amount of  data that the database would be responsible for at any given time, the `plant_events` table would be updated on the first of the month with the current month's schedule. Thus I needed to implement a recurring method to delete the past month's data and instantiate new database rows for the current month. 

![Calendar Component](https://media.giphy.com/media/W3eJJlxSRB9yM60Gx0/giphy.gif)

I followed [this detailed tutorial] (https://dev.to/risafj/cron-jobs-in-rails-a-simple-guide-to-actually-using-the-whenever-gem-now-with-tasks-2omi) for implementing my recurring job. The steps below show how I set a Cron job to run a Rake task once a month with the `whenever` gem.


## __Step 1. Add `whenever` gem and set up the `config/schedule.rb` file__ 
Add the [whenever] (https://github.com/javan/whenever) gem to your Gemfile with:
```
gem 'whenever', require: false
```
Then run `bundle` in the terminal.

After bundling, run `bundle exec wheneverize .` from the root of your Rails application. This creates a file called `config/schedule.rb` where you can write the schedule.


## __Step 2. Create the `whenever` schedule for your cron task in `config/schedule.rb`__
The `whenever` gem has several built-in shorthand calls for commonly used times. To run a task on the first of the month at midnight, you can simply use the built-in [`1.month` shortcut] (https://stackoverflow.com/questions/30287818/in-whenever-gem-if-we-use-every-month-does-it-mean-the-end-of-month-or-beginn):
```
# config/schedule.rb
every 1.month do
  rake 'calendar:update_calendar'
end
```
This will call a Rake task called `update_calendar` that is namespaced under `calendar`.


## __Step 3. Create a Rake task that will perform the needed task when called__
You can use a Rails generator to generate the Rake task file with:
```
rails g task calendar update_calendar
```
This creates a file called `lib/tasks/calendar.rake` with the following boilerplate code:
```
# lib/tasks/calendar.rake
namespace :calendar do
  desc "TODO"
  task update_calendar: :environment do
  end

end
```

Edit the `desc` line to describe what your task does and add the code to perform the task inside the block. Be sure to keep the `environment` line as that is what [loads your environment (including ActiveRecord models)] (https://stackoverflow.com/questions/876396/do-rails-rake-tasks-provide-access-to-activerecord-models) inside your task. The file now looks like:
```
namespace :calendar do
  desc "delete previous month's calendar events and populate current month"
  task update_calendar: :environment do
    PlantEvent.delete_all
    Plant.find_each do |plant|
      if plant.watering_repeat_rate_days
        plant.build_plant_events_collection(plant.watering_repeat_rate_days)
      end
    end
  end

end

```
In this case, all existing `plant_events` are deleted. Then each `plant` instantiates a collection of `plant_events` for the current month.

_For reference, the `Plant` model looks like:_
```
# app/models/plant.rb
require 'date'
require 'active_support'

class Plant < ApplicationRecord
  belongs_to :user
  has_many :plant_events, dependent: :destroy

  def build_plant_events_collection(watering_repeat_rate_days, start_date = Date.today)
    event_date = start_date

    if watering_repeat_rate_days
      while event_date < start_date.at_beginning_of_month.next_month
        self.plant_events.create(date: event_date, event_type: "water")
        event_date = event_date.advance(days: watering_repeat_rate_days)
      end
    end

  end
end
```


## __Step 4. Write the crontab file for your jobs__
Finally, write the crontab file that will check for cron jobs and run them in the background. You can read more about cron [here] (https://help.ubuntu.com/community/CronHowto) and [here] (https://en.wikipedia.org/wiki/Cron). Basically cron lets you run background jobs at specified intervals, and the crontab tracks these jobs by name. 

For production environments (the default) run:
```
bundle exec whenever --update-crontab 
```
When working in development, add the development flag `--set environment='development'` and run:
```
bundle exec whenever --update-crontab --set environment='development'
```
Now the cron job is all set to run the rake task once a month to update the calendar.