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.
Related
I've been wondering it is common to fetch records within initializer?
Here this is an example for service object to fetch records and generated pdf receipt file.
Input is invoice uuid, and fetch the related records such as card detail, invoice items within initialier.
class Pdf::GenerateReceipt
include Service
attr_reader :invoice, :items, :card_detail
def initialize(invoice_uuid)
#invoice ||= find_invoice!(invoice_uuid) # caching
#items = invoice.invoice_items
#card_detail = card_detail
end
.....
def call
return ReceiptGenerator.new(
id: invoice.uuid, # required
outline: outline, # required
line_items: line_items, # required
customer_info: customer_info
)
rescue => e
false, e
end
.....
def card_detail
card_metadata = Account.find(user_detail[:id]).credit_cards.primary.last
card_detail = {}
card_detail[:number] = card_metadata.blurred_number
card_detail[:brand] = card_metadata.brand
card_detail
end
end
Pdf::GenerateReceipt.('28ed7bb1-4a3f-4180-89a3-51cb3e621491') # => then generate pdf
The problem is if the records not found, this generate an error.
I could rescue within the initializer, however that seems not common.
How could I work around this in more ruby way?
This is mostly opinion and anecdotal, but I prefer to deal with casting my values as far up the chain as possible. So i would find the invoice before this object and pass it in as an argument, same with the card_detail.
If you do that in this class, it will limit the responsibility to coordinating those two objects, which is way easier to test but also adds another layer that you have to reason about in the future.
So how i would handle, split this into 4 separate things
Invoice Finder thing
Card Finder thing
Pdf Generator that takes invoice and card as arguments
Finally, something to orchestrate the 3 actions above
Hope this helps.
Addition: Check out the book confident ruby by avdi grimm. It's really great for outlining handling this type of scenario.
I have ruby on rails app and my controller should process request which creates many objects. Objects data is passed from client via json using POST method.
Example of my request (log from controller):
Processing by PersonsController#save_all as JSON
Parameters: {"_json"=>[{"date"=>"9/15/2014", "name"=>"John"},
{"date"=>"9/15/2014", "name"=>"Mike"}], "person"=>{}}
So i need to save these two users but i have some issues:
How to verify strong parameters here? Only Name and Date attributes can be passed from client
How can I convert String to Date if i use Person.new(params)?
Can i somehow preprocess my json? For example i want to replace name="Mike" to name="Mike User" and only then pass it in my model
I want to enrich params of every person by adding some default parameters, for example, i want to add status="new_created" to person params
First of all I'd name the root param something like "users", then it gives a structure that is all connected to the controller name and the data being sent.
Regarding strong params. The config depends of your rails app version. <= 3.x doesn't have this included so you need to add the gem. If you're on >= 4.x then this is already part of rails.
Next in your controller you need to define a method that will do the filtering of the params you need. I should look something like:
class PeopleController < ApplicationController
def some_action
# Here you can call a service that receives people_params and takes
# care of the creation.
if PeopleService.new(people_params).perform
# some logic
else
# some logic
end
end
private
def base_people_params
params.permit(people: [:name, :date])
end
# Usually if you don't want to manipulate the params then call the method
# just #people_params
def people_params
base_people_params.merge(people: normalized_params)
end
# In case you decided to manipulate the params then create small methods
# that would that separately. This way you would be able to understand this
# logic when returning to this code in a couple of months.
def normalized_params
return [] unless params[:people]
params[:people].each_with_object([]) do |result, person|
result << {
name: normalize_name(person[:name]),
date: normalize_date(person[:date]),
}
end
end
def normalize_date(date)
Time.parse(date)
end
def normalize_name(name)
"#{name} - User"
end
end
If you see that the code starts to get to customized take into a service. It will help to help to keep you controller thin (and healthy).
When you create one reason at the time (and not a batch like here) the code is a bit simpler, you work with hashes instead of arrays... but it's all pretty much the same.
EDIT:
If you don't need to manipulate a specific param then just don't
def normalized_params
return [] unless params[:people]
params[:people].each_with_object([]) do |result, person|
result << {
name: person[:name],
date: normalize_date(person[:date]),
}
end
end
I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
I have this method in my reports_controller.rb, which allows an user to send a status.
def send_status
date = Date.today
reports = current_user.reports.for_date(date)
ReportMailer.status_email(current_user, reports, date).deliver
head :ok
rescue => e
head :bad_request
end
How can I call this action from ActiveAdmin, in order to check if a User sent this report or not? I want it like a status_tag on a column or something.
Should I do a member action?
Thanks!
I'll address the issue of checking if a report has been sent later, but first I'll cover the question of how to call the controller action from ActiveAdmin.
While you can call ReportsController#send_status by creating an ActionController::Base::ReportsController and then calling the desired method, e.g.
ActionController::Base::ReportsController.new.send_status
this isn't a good idea. You probably should refactor this to address a couple potential issues.
app/controllers/reports_controller.rb:
class ReportsController < ApplicationController
... # rest of controller methods
def send_status
if current_user # or whatever your conditional is
ReportMailer.status_email(current_user).deliver
response = :ok
else
response = :bad_request
end
head response
end
end
app/models/user.rb:
class User < ActiveRecord::Base
... # rest of user model
def reports_for_date(date)
reports.for_date(date)
end
end
app/mailers/reports_mailer.rb
class ReportsMailer < ActionMailer::Base
... # rest of mailer
def status_email(user)
#user = user
#date = Date.today
#reports = #user.reports_for_date(#date)
... # rest of method
end
end
This could obviously be refactored further, but provides a decent starting point.
An important thing to consider is that this controller action is not sending the email asynchronously, so in the interest of concurrency and user experience, you should strongly consider using a queuing system. DelayedJob would be an easy implementation with the example I've provided (look into the DelayedJob RailsCast).
As far as checking if the report has been sent, you could implement an ActionMailer Observer and register that observer:
This requires that the User model have a BOOLEAN column status_sent and that users have unique email address.
lib/status_sent_mail_observer.rb:
class StatusSentMailObserver
self.delivered_email(message)
user = User.find_by_email(message.to)
user.update_attribute(:status_sent, true)
end
end
config/intializer/setup_mail.rb:
... # rest of initializer
Mail.register_observer(StatusSentMailObserver)
If you are using DelayedJob (or almost any other queuing system) you could implement a callback method to be called on job completion (i.e. sending the status email) that updates a column on the user.
If you want to track the status message for every day, you should consider creating a Status model that belongs to the User. The status model could be created every time the user sends the email, allowing you to check if the email has been sent simply by checking if a status record exists. This strategy is one I would seriously consider adopting over just a simple status_sent column.
tl;dr ActionController::Base::ReportsController.new.send_status & implement an observer that updates a column on the user that tracks the status. But you really don't want to do that. Look into refactoring like I've mentioned above.
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.