I have the following models:
class Page < ApplicationRecord
has_one :architecture
end
class Architecture < ApplicationRecord
belongs_to :page
end
And after a new page is saved I need to capture it's architecture (number of paragraphs por example). I would like to know what is the proper way to do that. I'm not sure if I should leave that responsible for the Page model:
class Page < ApplicationRecord
has_one :architecture
after_create :scrape_architecture
private
def scrape_architecture
data = call_something_to_capture_architecture(url)
create_architecture(data)
end
end
class Architecture < ApplicationRecord
belongs_to :page
end
or if it should be the responsibility of the Architecture model:
class Page < ApplicationRecord
has_one :architecture
after_create :create_architecture
end
class Architecture < ApplicationRecord
belongs_to :page
before_create :scrape_page
private
def scrape_page
data = call_something_to_capture_architecture(page.url)
create(data)
end
end
Which is actually incorrectly because before_create runs after the validation – causing MySQL errors duo to non null constraints
Thank you.
I would just create a job or service object that handles scraping.
class PageScrapingJob < ApplicationJob
queue_as :default
def perform(page)
data = call_something_to_capture_architecture(page.url)
architecture = page.create_actitecture(data)
# ...
end
end
You would then call the service/job in your controller after saving the page:
class PagesController < ApplicationController
def create
#page = Page.new(page_params)
if #page.save
PageScrapingJob.perform_now(#page)
redirect_to #page
else
render :new
end
end
end
This gives you a perfect control of exactly when this is fired and avoids placing even more responsibilities onto your models. Even though your models may contain little code they have a huge amount of responsibilities such as validations, I18n, form binding, dirty tracking etc that are provided by ActiveModel and ActiveRecord. The list really goes on and on.
This instead creates a discrete object that does only one job (and hopefully does it well) and that can be tested in isolation from the controller.
For such things you could use a service pattern
class PageScrapper
Result = Struct.new(:success?, :data)
def initialize(url)
#url = url
end
def call
result = process(#url)
...
if result.success? # pseudo methods
Result.new(true, result)
else
Result.new(false, nil)
end
end
end
class Fetcher
...
def call
scrapper = PageScrapper.new(url)
result = scrapper.call
if scrapper.success?
page = Page.build(parsed_result_if_needed(result)
page.architecture.build(what_you_need)
page.save # here you need to add error handling if save fails
else
# error handling
end
end
There are a lot of resources about why callbacks are bad.
Here is one from Marcin Grzywaczewski but you can also google it "why callbacks are bad ruby on rails".
By using service you are liberating models from having too much business logic and they do not need to know about other parts of your system.
Related
I'm working on a fitness tracking app where we encourage you to track a habit for 30 days.
Every user has_many projects, projects belong_to user, projects has_many tasks, and tasks belong_to projects.
What I'm looking to do is when a project is created, I want to populate 30 empty tasks which will be displayed in order of day, and allow the user to click on a day and update the task. (see image)
enter image description here
I'm happy to post any of the code/views if you need reference.
Thanks for the help!
class Project < ActiveRecord::Base
has_many :tasks
def create_tasks!(n = 30)
self.class.transaction do
1..n.each do |day|
self.tasks.create(day: day)
end
end
end
end
Wrapping a mass insert in a single transaction is vital for performance - otherwise each insert will be run in its own transaction.
You could use a after_create model callback to call create_tasks! - but this can be problematic since the callback will fire every time you create a project which can make tests slow.
class Project < ActiveRecord::Base
has_many :tasks
after_create :create_tasks!
def create_tasks!(n = 30)
self.class.transaction do
1..n.each do |day|
self.tasks.create(day: day)
end
end
end
end
Another way to this would be to call it in your controller:
class ProjectsController < ApplicationController
def create
#project = Project.new(project_params)
if #project.save
#project.create_tasks!
redirect_to #project
else
render :new
end
end
end
Which gives you better control over exactly where in the application it happens.
You can make use of Active Record's after_create callback, which allows you to perform a task whenever a new record is created for a certain model:
class Project < ActiveRecord::Base
has_many :tasks
after_create :create_empty_tasks
private
def create_empty_tasks
# Create your 30 Task objects
30.times do |i|
Task.create(day: (i + 1), project: self) # Update to match your schema
end
end
end
You'll of course need to update that code to pass any user-specific data into the Task, but hopefully the callback is a good starting point.
I'm writing a simple Rails app and I'm wondering what to name a controller that creates accounts.
Some background: Users create a schedule and it's publicly visible. There are users and events. An event can have multiple event_sessions. That's pretty much it.
During registration a user is created, an event is created, and sessions are created. So where do I put this, the UsersController? And, if account creation includes all this other stuff, do I put it in a new controller? If so, what do I call the controller — ExternalSiteController? AccountController?
I would start with something like the following, and tweak as necessary:
class UsersController < ActionController::Base
def create
# ...
User.create(user_params)
# ...
end
end
class User < ActiveRecord::Base
after_create :setup_initial_event
has_many :events
DEFAULT_EVENT_PARAMS = {
# ...
}
def setup_initial_event
events.create(DEFAULT_EVENT_PARAMS)
end
end
class Event < ActiveRecord::Base
after_create :setup_initial_sessions
belongs_to :user
has_many :sessions
def setup_initial_sessions
# You get the idea
end
end
If you don't have an account model (in which case AccountsController would be perfect), I'd put the code in the UsersController. User is probably the most complex and important model of the three (the registration of a user is what's kicking everything off, after all). Of course, you can create any object in any controller (i.e. you can call User.create() in the EventsController).
Many presenters, have something of the form of
class MyClassPresenter < SimpleDelegator
def something_extra
end
end
view_obj = MyClassPresenter.new my_class_instance
I want to transverse the instance:
view_obj.nested_obj.nested_obj.value
This means creating multiple presentation objects, which in effect start just copying the models.
class MyClassPresenter < SimpleDelegator
def something_extra
end
def nest_obj
AnotherPresenter.new(__get_obj__.nest_obj)
end
end
To demonstrate a real world example a bit better
class UserPresenter < SimpleDelegator
def home_page_url
"/home/#{__get_obj__.id}"
end
end
class MyController < ApplicationController
def show
#user = UserPresenter.new(current_user) # devise's current_user
end
end
/my/show.html.slim
/! Show profile comments
- for comment in #user.profile.comments
| Comment on:
= comment.created_at
= comment.content
The main object being passed in is #user, however, the presenter doesn't cover that far. I could create #comments, but I would like the code to be more flexible to however the front-end engineers wanted to take it.
How have other people handled multiple layers for a presenter?
Would the code look something like this? (ugh)
/! Show profile comments
- for comment in #user.profile.comments
- display_comment = CommentPresenter.new(comment)
| Comment on:
= display_comment.comment.created_at
= display_comment.content
-daniel
I'm using single table inheritance successfully like so:
class Transaction < ActiveRecord::Base
belongs_to :order
end
class Purchase < Transaction
end
class Refund < Transaction
end
The abbreviated/simplified PurchaseController looks like this:
class PurchaseController < TransactionController
def new
#transaction = #order.purchases.new(type: type)
end
def create
#transaction = #order.purchases.new secure_params
if #transaction.save
redirect_to #order
else
render :new
end
end
end
The abbreviated/simplified Purchase model looks like this:
class Purchase < Transaction
attr_accessor :cc_number, :cc_expiry, :cc_csv
end
What I'm trying to do is have different variations of a purchase, for instance a cash purchase & a cheque purchase. The issue is I'm not sure how to call the model for that variation.
For example:
class Cash < Purchase
attr_accessor :receipt_no
end
class CashController < TransactionController
def new
# This will use the Purchase.rb model so that's no good because I need the Cash.rb model attributes
#transaction = #order.purchases.new(type: type)
# This gives me the following error:
# ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Purchase is not a subclass of Cash
#transaction = Cash.new(type: 'Purchase', order: #order.id)
end
end
I'm not sure why it doesn't work for you, this works fine for me:
#order.purchases.new(type: "Cash") # returns a new Cash instance
You can also push a new Cash on to the association if you are ready to save it:
#order.purchases << Cash.new
Or you can define a separate association in Order:
class Order < ActiveRecord::Base
has_many :cashes
end
#order.cashes.new # returns a new Cash instance
Class
Maybe I'm being obtuse, but perhaps you'll be willing to not make the Purchase type an inherited class?
The problem I see is that you're calling Cash.new, when really you may be better to include all the functionality you require in the Purchase model, which will then be able to be re-factored afterwards.
Specifically, why don't you just include your own type attribute in your Purchase model, which you'll then be able to use with the following setup:
#app/controllers/cash_controller.rb
class CashController < ApplicationController
def new
#transaction = Purchase.new
end
def create
#transaction = Purchase.new transaction_params
#transaction.type ||= "cash"
end
private
def cash_params
params.require(:transaction).permit(:x, :y, :z)
end
end
The only downside to this would be that if you wanted to include different business logic for each type of purchase, you'll still want to use your inherited model. However, you could simply split the functionality in the before_create callback:
#app/models/puchase.rb
class Purchase < Transaction
before_create :set_type
private
def set_type
if type =="cash"
# do something here
else
# do something here
end
end
end
As such, right now, I think your use of two separate models (Cash and Cheque) will likely be causing much more of an issue than is present. Although I'd love to see how you could inherit from an inherited Model, what I've provided is something you also may wish to look into
I am using Ruby on Rails 3.2.2 and I would like to know what advantages / disadvantages the following code has or could have:
class ApplicationController < ActionController::Base
before_filter :set_current_user_for_models
private
def set_current_user_for_models
User.current_user = User.find(...) # Find the user from cookies.
end
end
class User < ActiveRecord::Base
attr_accessible :current_user
private
def some_method(user)
if User.current_user == user
# Make a thing...
else
# Make another thing...
end
end
end
class Article < ActiveRecord::Base
def some_method(user)
if User.current_user == user
# Make a thing...
else
# Make another thing...
end
end
end
Have you some advice? How would you improve the code?
Updated after the #tokland comment on the #Wawa Loo answer
Note: the main difference is that User.current_user should be updated also in after_find, after_create, ... model callbacks. Something like this:
class User < ActiveRecord::Base
attr_accessible :current_user
after_initialize :some_method
private
def some_method
if User.current_user == self
# Make a thing...
else
# Make another thing...
end
end
end
class Article < ActiveRecord::Base
after_destroy :some_method
private
def some_method
if User.current_user == self.user
# Make a thing...
else
# Make another thing...
end
end
end
Your code looks fine. I recommend you to read through this rails style guide for an overview on Rails development.
I recommend storing the current_user in your session, not as an accessor for the class User. The method set_current_user_for_models will be executed with each request. How does it find the user anyway - User.find(...)?