There are several stages to this, and as I am relatively new to rails I am unsure if I am approaching this in the best way.
Users follow Firms, Firms applications open and close on certain days. If a user follows a firm I would like them to automatically get an email when a) the firms application opens, b) a week before the firms applications close, c) on the day that the firms applications close.
I have tried using named scope. I have the following model method (I presume this will need a little work) setting each firms scope, depending on the date.
model firms.rb
def application_status
if open_date == Today.date
self.opening = true
else
self.opening = false
end
if ((close_day - Today.date) == 7)
self.warning = true
else
self.warning = false
end
if close_day == Today.date
self.closing = true
else
self.closing = false
end
end
I would like this method to be called on each firm once a day, so that each firm has the appropriate scope - so I have tried using the whenever gem (cron) and the following code. Running the above model method on each firm.
Schedule.rb
every 1.day do
runner "Firm.all.each do |firm|
firm.application_status
end"
end
Then for each of the scopes opening, warning, closing i have a method in the whenever schedules file, For simplicity I shall show just the opening methods. The following queries for all firms that have had the opening scope applied to them, and runs the application_open_notification method on them.
Schedule.rb
every 1.day do
runner "Firm.opening.each do |firm|
firm.application_open_notification
end"
end
This calls the following method in the Firm.rb model
def application_open_notification
self.users.each do |user|
FirmMailer.application_open(user, self).deliver
end
end
Which in turn calls the final piece of the puzzle... which should send the user an email, including the name of the firm.
def application_open(user,firm)
#firm = firm
#user = user
mail to: #user.email, subject: #firm' is now accepting applications'
end
end
Is this a viable way to approach this problem? In particular I am not very familiar with coding in the model.
Many thanks for any help that you can offer.
I'll guess that opening, warning and closing are database fields, and you have scopes like:
class Firm < ActiveRecord::Base
scope :opening, :where => { :opening => true }
# etc
end
There is a general rule for database (and, well, all storage): don't store things you can caculate, if you don't have to.
Since an application's status can be dermined from the day's date and the open_date and close_day fields, you could calculate them as needed instead of creating extra fields for them. You can do this with SQL and Active Record:
scope :opening, :where { :open_date => (Date.today .. Date.today+1) }
scope :warning, :where { :close_day => (Date.today+7 .. Date.today+8) }
scope :closing, :where { :close_day => (Date.today .. Date.today+1) }
(Note that these select time ranges. They may have to be changed depending on if you are using date or time fields.)
But there is another issue: what happens if, for some reason (computer crash, code bug etc) your scheduled program doesn't run on a particular day? You need a way of making sure notices are sent eventually even if something breaks. There are two solutions:
Write your schedule program to optionally accept a date besides today (via ARGV)
keep flags for each firm for whether each kind of notice has been sent. These will have to be stored in the databse.
Note that scopes aren't necessary. You are able to do this:
Firm.where(:open_date => (Date.today .. Date.today+1)).each do |firm|
#...
end
but the scope at least encapsulates the details of identifying the various sets of records.
Related
I building a rewards system for a coffee shop. Basically a customer can sign up for a year subscription. Right now when they sign up the active attribute is toggled to true. I'm trying to write a method that will toggle the attribute to false after a year passes. I have a method right now that I want to use but I don't know where to use it at? I also have a failing test. I'll show my current code for clarity.
Controller:
def create
#subscriber = Subscriber.new(subscriber_params)
if #subscriber.save
#subscriber.touch(:subscription_date)
#subscriber.update(active: true)
SubscriberMailer.welcome_subscriber(#subscriber).deliver_now
flash[:notice] = "Subscriber Has Been Successfully Created"
redirect_to new_subscriber_path(:subscriber)
else
render "new"
end
end
Model method I want to use:
def not_active(subscriber)
if subscription_date < 1.year.ago
self.update(active: false)
end
end
Failing Test:
it "sets active to false after a year" do
subscriber = create(:subscriber)
subscriber.update(active: true)
Time.now + 366.days
expect(subscriber.active).to eq(false)
end
So hopefully this idea is clear. I just want to update to active: false if the user was created over a year ago.
You must run the not_active method in order for the method to have an effect. The method has no way of knowing what the date is today and updating a subscriber unless it is actually run. I agree with matt that you would likely run this method in a sidekiq job daily on on all of your subscribers who subscribed a year or longer ago and are active (You can write a scope for this). This way you can call the not_active method and set each subscriber's active appropriately, or write it as a Subscriber class method and apply it to the results of your scope. In the case of testing the not_active method itself all you need to do is call it and test the result. Its also not clear to me why the not_active method takes a subscriber as an arg, it seems like it would make more sense to just call it from a subscriber instance. Is this not whats already happening? I would personally call this method something like deactivate!, as its making changes. not_active kind of sounds like it would return a boolean or an inactive subscriber. I would also recommend using update! instead of update in not_active. update! will raise an error if the update fails. Adding to time.now does actually change the time. You can use rspec mocks to fake the current time if you need to. In any case here is what your not_active test might look like:
it "sets active to false after a year" do
subscriber = Subscriber.create(subscription_date: (1.year.ago - 1.day), active: true)
#changed not_active to deactivate, called from instance instead of passing in subscriber
subscriber.deactivate!
expect(subscriber.active?).to eq(false)
end
You can also write a test for the other case
it "does not deactivate a recent subscriber" do
subscriber = Subscriber.create(subscription_date: Date.today, active: true)
subscriber.deactivate!
expect(subscriber.active?).to eq(true)
end
A simple solution to this would be to use cron. There is a rubygem to interface with cron, called whenever. The setup is simple and well documented.
With cron setup on your server, you would create some kind of class method that would iterate through Subscribers, calling the not_active method.
Btw, if the not_active method is defined within your Subscriber model, you won't need to pass subscriber as an argument, as self will be implicitly set to the subscriber.
The code would end up looking something like:
in subscriber.rb
def self.set_subscribers_to_inactive
find_each(active: false) do |subscriber|
subscriber.inactive!
end
end
def inactive!
update(active: false) if subscription_date < 1.year.ago
end
in schedule.rb
every 1.day do
runner "Subscriber.set_subscribers_to_inactive"
end
As mentioned, your test is not actually calling the not_active method.
it "sets active to false after a year" do
last_year = DateTime.current - 366.days
subscriber = create(:subscriber, active: true, subscription_date: last_year)
subscriber.inactive!
expect(subscriber.active).to eq false
end
Take a look at cron and whenever gem which works on top of cron. You just need to write a super simple script which will extract data from DB and update it.
Another way to solve your problem is not to update anything. You only need *_expires_at column and check if its value less than current date.
It is pretty agile method, because by using activation_expires_at column you are able to implement #active? method and .active scope to select only users with active subscriptions.
I'm writing an application where user enters a date, and then the system fetches the historical weather data for that week (I assume that Wednesday is representative for the whole week) from an external API. For certain reasons, I don't want to do live calls for each date - I want to fetch it once and persist on-site.
In Spring, I'd put most of it into a service layer. Since I am new to Rails, I am not sure where to put certain logic, but here's my proposal:
WeatherController
def create
transform date entered by user to Wednesday of the same week.
Check if there is a already record for that date, if not, fetch the JSON from external API.
Parse JSON to Ruby object, save.
Return the weather data.
WeatherModel
validate if the date is indeed Wednesday
validate if entered date is unique
Generally, I wouldn't put the logic in a create action. Even though you're creating something, the user of your site is really asking you to show the weather. The user should be oblivious to where you're bringing the info from and how you're caching it.
Option 1 - Use Rails Caching
One option is to use Rails caching in the show action. Right in that action you will do a blocking call to the API, and then Rails will store the return value in the cache store (e.g. Redis).
def show
date = Date.parse params[:date]
#info_to_show = Rails.cache.fetch(cache_key_for date) do
WeatherAPIFetcher.fetch(date)
end
end
private
def cache_key_for(date)
"weather-cache-#{date + (3 - date.wday)}"
end
Option 2: Non-blocking calls with ActiveJobs
Option 1 above will make accessing the data you already accumulated somewhat awkward (e.g. for statistics, graphs, etc). In addition, it blocks the server while you are waiting for a response from the API endpoint. If these are non-issues, you should consider option 1, as it's very simple. If you need more than that, below is a suggestion for storing the data you fetch in the DB.
I suggest a model to store the data and an async job that retrieves the data. Note you'll need to have ActiveJob set up for the WeatherFetcherJob.
# migration file
create_table :weather_logs do |t|
t.datetime :date
# You may want to use an enumerized string field `status` instead of a boolean so that you can record 'not_fetched', 'success', 'error'.
t.boolean :fetch_completed, default: false
t.text :error_message
t.text :error_backtrace
# Whatever info you're saving
t.timestamps
end
add_index :weather_logs, :date
# app/models/weather_log.rb
class WeatherLog
# Return a log record immediately (non-blocking).
def self.find_for_week(date_str)
date = Date.parse(date_str)
wednesday_representative = date + (3 - date.wday)
record = find_or_create_by(date: wednesday_representative)
WeatherFetcherJob.perform_later(record) unless record.fetch_completed
record
end
end
# app/jobs/weather_fetcher_job.rb
class WeatherFetcherJob < ActiveJob::Base
def perform(weather_log_record)
# Fetch from API
# Update the weather_log_record with the information
# Update the weather_log_record's fetch_completed to true
# If there is an error - store it in the error fields.
end
end
Then, in the controller you can rely on whether the API completed to decide what to display to the user. These are broad strokes, you'll have to adapt to your use case.
# app/controllers/weather_controller
def show
#weather_log = WeatherLog.find_for_week(params[:date])
#show_spinner = true unless #weather_log.fetch_completed
end
def poll
#weather_log = WeatherLog.find(params[:id])
render json: #weather_log.fetch_completed
end
# app/javascripts/poll.js.coffee
$(document).ready ->
poll = ->
$.get($('#spinner-element').data('poll-url'), (fetch_in_progress) ->
if fetch_in_progress
setTimeout(poll, 2000)
else
window.location = $('#spinner-element').data('redirect-to')
)
$('#spinner-element').each -> poll()
# app/views/weather_controller.rb
...
<% if #show_spinner %>
<%= content_tag :div, 'Loading...', id: 'spinner-element', data: { poll_url: poll_weather_path(#weather_log), redirect_to: weather_path(#weather_log) } %>
<% end %>
...
In rails I prefer to create POROs (plan old ruby objects) to handle most of the core logic in my applications. In doing so we can keep our controllers dead simple and our models void of logic that does not pertain to saving data to the database. If you don't work at keeping unnecessary logic out of of our models they will become bloated and extremely hard to test.
The two PORO patterns I use the most are actions and services.
actions normally relate directly to and assist one controller action.
To take your example lets create one. We will create a WeatherCreator class. I like names that are insanely explicit. What does WeatherCreator do you ask? It creates a Weather record, of course!
# app/actions/weather_creator.rb
class WeatherCreator
attr_reader :weather
def initialize(args={})
#date = args.fetch(:date)
#weather = Weather.new
end
def create
build_record
#weather.save
end
private
def build_record
# All of your core logic goes here!
# Plus you can delegate it out to various private methods in the class
#
# transform date entered by user to Wednesday of the same week.
# Check if there is a already record for that date, if not, fetch the JSON from external API.
# Parse JSON to Ruby object, save.
#
# Add necessary data to your model in #weather
end
end
Then in our controller we can use the action class
# app/controllers/weather_controller.rb
class WeatherController < ApplicatonController
def create
creator = WeatherCreator.new(date: params[:date])
if creator.create
#weather = creator.weather
render :new
else
flash[:success] = "Weather record created!"
redirect_to some_path
end
end
end
Now your controller is stupid simple.
The great benefit of this is that your testing efforts can focus just on the action logic object and it's interface.
# spec/actions/weather_creator_spec.rb
require 'rails_helper'
RSpec.describe WeatherCreator do
it "does cool things" do
creator = WeatherCreator.new(date: Time.zone.now)
creator.create
expect(creator.weather).to # have cool things
end
end
service objects on the other hand would live in app/services/. The difference is that these objects are used in many places in an app but the same isolation of logic and testing practices apply.
Depending on your app you can create different types of POROS for various purposes as a general service object category can also grow out of control.
To make things clear you can utilize different naming practices. So we could take the WeatherCreator class and instead call it WeatherCreatorAction or Action::WeatherCreator. Some goes with services SomeLogicService or Service::SomeLogic.
Use whatever suites your preferences and style best. Cheers!
I will give you little interesting way to implement easy and interesting way. You can make it like bookmark logic:
For example:
How's bookmark work ? User adds an URL to bookmarks, server saves the data of that bookmark, and when another user tries to add the same URL to bookmark, server not saves URL to bookmark because its duplicated Bookmark. Its just, server finds that bookmark and assigns to that user too. and again again again for all users who tries to add that the same url to bookmark.
Weather:
In your case, all you need is: If user request weather of that city and if you dnt have that data then fetch from api give it to user and save it to DB. and if another will request the same city, now just responding from DB not from 3rd party API. All you need is update the data, when it gets requested.
I'm currently upgrading an existing application from rails 3 to 4. I've encountered changed behaviour, but I'm not sure how to fix this.
say the following exists;
class Program
has_many :pauses
end
class Pause
belongs_to :program
def dates
(starts_at...ends_at)
end
validate :validate_without_overlap
def validate_without_overlap
return if (program.pauses.map(&:dates).flatten & [starts_at, ends_at]).blank?
# set errors...
end
end
program = Program.create
program.pauses.build starts_at: 1.week.ago.to_date, ends_at: Date.today
# ...
program.save
To verify the pause does not have a overlap with existing pauses, within a
validation method the following happens:
program.pauses.map(&:dates)
This already includes the builded pause record. Which triggers a validation error because it overlaps itself. How to kill groundhog day?
Just exclude new records use something like this:
program.pauses.select { |o| !o.new_record? }.map(&:dates)
or
program.pauses.reject(&:new_record?).map(&:dates)
Relevant Code: http://pastebin.com/EnLJUJ8G
class Task < ActiveRecord::Base
after_create :check_room_schedule
...
scope :for_date, lambda { |date| where(day: date) }
scope :for_room, lambda { |room| where(room: room) }
scope :room_stats, lambda { |room| where(room: room) }
scope :gear_stats, lambda { |gear| where(gear: gear) }
def check_room_schedule
#tasks = Task.for_date(self.day).for_room(self.room).list_in_asc_order
#self_position = #tasks.index(self)
if #tasks.length <= 2
if #self_position == 0
self.notes = "There is another meeting in
this room beginning at # {#tasks[1].begin.strftime("%I:%M%P")}."
self.save
end
end
end
private
def self.list_in_asc_order
order('begin asc')
end
end
I'm making a small task app. Each task is assigned to a room. Once I add a task, I want to use a callback to check to see if there are tasks in the same room before and or after the task I just added (although my code only handles one edge case right now).
So I decided to use after_create (since the user will manually check for this if they edit it, hence not after_save) so I could use two scopes and a class method to query the tasks on the day, in the room, and order them by time. I then find the object in the array and start using if statements.
I have to explicitly save the object. It works. But it feels weird that I'm doing that. I'm not too experienced (first app), so I'm not sure if this is frowned upon or if it is convention. I've searched a bunch and looked through a reference book, but I haven't see anything this specific.
Thanks.
This looks like a task for before_create to me. If you have to save in your after_* callback, you probably meant to use a before_* callback instead.
In before_create you wouldn't have to call save, as the save happens after the callback code runs for you.
And rather than saving then querying to see if you get 2 or more objects returns, you should be querying for one object that will clash before you save.
In psuedo code, what you have now:
after creation
now that I'm saved, find all tasks in my room and at my time
did I find more than one?
Am I the first one?
yes: add note about another task, then save again
no: everything is fine, no need to re-save any edits
What you should have:
before creation
is there at least 1 task in this room at the same time?
yes: add note about another task
no: everything is fine, allow saving without modification
Something more like this:
before_create :check_room_schedule
def check_room_schedule
conflicting_task = Task.for_date(self.day)
.for_room(self.room)
.where(begin: self.begin) # unsure what logic you need here...
.first
if conflicting_task
self.notes =
"There is another meeting in this room beginning at #{conflicting_task.begin.strftime("%I:%M%P")}."
end
end
I am developing a Rails web application and am confused about how to utilize the lookup table values in my models. Here is an example model from my app:
table name: donations
id
amount
note
user_id
appeal_id
donation_status_id
donation_type_id
is_anonymous
created_at
updated_at
The fields *donation_status_id* and *donation_type_id* refer to lookup tables. So in my code I have several random places where I make calls like this:
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus.find_by_name("completed").id
#do something
end
To my inexperienced eyes, a one-off query to the DonationStatus table seems incredibly wasteful here, but I don't see any other good way to do it. The first idea I thought of was to read all my lookup tables into a hash at application startup and then just query against that when I need to.
But is there a better way to do what I am trying to do? Should I not worry about queries like this?
Thanks!
Since you have two models, you should use ActiveRecord Model Associations when building the models.
class Donation
has_one :donation_status
end
class DonationStatus
belongs_to :donation
end
Then when you do
my_donation = Donation.find(params[:id])
if my_donation.donation_status.status_name == 'complete'
#do something
end
For more information, you may want to read up how rails is doing the model associations http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html Don't worry about performance, rails has taken care of that for you if you follow how the way it should be done
How about putting it in a constant? For example, something like this:
class DonationStatus < ActiveRecord::Base
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id
PENDING_DONATION_ID = DonationStatus.find_by_name("pending").id
# ...
end
class DonationsController < ApplicationController
def some_action
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus::COMPLETED_DONATION_ID
#do something
end
end
This way, DonationStatus.find_by_name("pending").id gets executed exactly one. I'm assuming, of course, that this table won't change often.
BTW, I learned this trick in Dan Chak's book, Enterprise Rails.
EDIT: I forgot to mention: in practice, I declare constants like this:
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id rescue "Can't find 'completed' in donation_statuses table"
What you could do is add this method to Donation:
# Donation.rb
def completed?
self.donation_status.name == 'completed' ? true : false
end
And then just do my_donation.completed?. If this is called a second time, Rails will look to cache instead of going to the DB.
You could add memcached if you want, or use Rails' caching further, and do:
def completed?
return Rails.cache.fetch("status_#{self.donation_status_id}_complete") do
self.donation_status.name == 'completed' ? true : false
end
end
What that will do is make a hash key called (for example) "status_1_complete" and if it's not defined the first time, will evaluate the block and set the value. Otherwise, it will just return the value. That way, if you had 1,000,000,000 donations and each of them had donation_status 1, it would go directly to the cache. memcached is quite fast and popular.