Render RABL partials with polymorphic models - ruby-on-rails-3

I have a data model where a User can like a Project, Suggestion, Comment, or other objects.
The models are set up correctly, and likes/show.rabl works if we were just to support the Project child
object #like
attributes :id, :created_at, :target_type, :target_id, :user_id
child :user => :user do
extends "users/show"
end
child :target do
node do |project|
partial('projects/show', :object => project)
end
end
However, I want to be able to use partials of suggestions/show, comments/show as well depending on target_type.
I tried this but it's not working:
child :target do |u|
u.target.map do |r|
if r.is_a?(Suggestion)
partial("suggestions/show", :object => r)
elsif r.is_a?(Project)
partial("projects/show", :object => r)
end
end
end
I get undefined method target for <Rabl::Engine:0x69fb988>. However, I don't get this error in the first case. Any ideas?

Have you tried using extends instead of partial?
Perhaps you could try something like this?
child :target do |u|
u.target.map do |r|
if r.is_a?(Suggestion)
extends "suggestions/show"
elsif r.is_a?(Project)
extends "projects/show"
end
end
end
In this case, when you use extends, you don't need to pass in an :object because you're already in the scope of iterating through with the object r.

This is what I ended up using and it worked.
Thanks to this post: https://github.com/nesquena/rabl/issues/273#issuecomment-6580713
child :target do
node do |r|
if r.is_a?(Suggestion)
partial("suggestions/show", :object => r)
elsif r.is_a?(Project)
partial("projects/show", :object => r)
end
end
end

There is another way to do this, where you do not have to list all possible polymorphic relations. It may be a bit hacky, but it works.
Like this:
child :target do |u|
# The regexp snippet selects everything from the last / and to the end
extends u.to_partial_path.gsub(/\/([^\/]+)$/, '/show')
end
This uses the .to_partial_path of your object. If your object is called Suggestion, the .to_partial_path will return suggestions/suggestion. This is why i'm using gsub() to replace whatever comes after / with /show.
With this solution you do not have to update this file each time you add another polymorphic relation. You only have to make sure that the new object has a rabl template called show.json.rabl

Related

Validating Child Object with ActiveModel Validations

I have two plain Ruby classes, Account and Contact. I am using Simple Form's simple_form_for and simple_fields_for to create nested attributes. I am looking to fulfill the following validation requirements:
An associated Contact must exist for the new Account
The associated Contact must be valid (i.e., account.contact.valid?)
It looks like ActiveModel no longer includes the validates_associated method, as using that method results in an undefined method error. I considered requiring ActiveRecord::Validations, but this led down a stretch of various errors (e.g., undefined method `marked_for_destruction?')
I also considered defining validate on the Account class and calling valid? on the associated object, but that only prevented the form from submitting if there was also an error on the parent object.
validate do |account|
account.contact.valid?
# required for form to fail
errors.add(:base, "some error")
end
Is there something I'm not aware of to solve this? Thanks.
I recently (7 years after this question has been asked!) faced the same issue and solved it by implementing the AssociatedValidator based on the ActiveRecord one.
I simply included it in config/initializers folder:
module ActiveModel
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if Array(value).reject { |r| valid_object?(r) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
end
end
private
def valid_object?(record)
record.valid?
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
now you can use validates_associated in ActiveModel too.
class Person
include Virtus
include ActiveModel::Model
attribute :address, Address, :default => Address.new
validate :address_valid
private
def address_valid
errors.add(:base, 'address is not valid') unless address.valid?
end
end
class Address
include Virtus::ValueObject
include ActiveModel::Validations
attribute :line_1, String
attribute :line_2, String
validates :line_1, :presence => true
validates :line_2, :presence => true
end
The errors show up in the form if you pass an object to simple_fields_for:
= form.simple_fields_for person.address do |af|
= af.input :line_1
Another option is overriding valid?:
def valid?
super & address.valid?
end
Note its & not && so the conditions are not short circuited if the first returns false.

Null Object Pattern for associations in Rails

Despite looking at a few answers here regarding Null Objects in rails, I can't seem to get them to work.
class User < ActiveRecord::Base
has_one :profile
accepts_nested_attributes_for :profile
def profile
self.profile || NullProfile #I have also tried
#profile || NullProfile #but it didn't work either
end
end
class NullProfile
def display #this method exists on the real Profile class
""
end
end
class UsersController < ApplicationController
def create
User.new(params)
end
end
My problem is that on User creation, I pass in the proper nested attributes (profile_attributes) for the Profile and I end up with a NullProfile on my new User.
I am guessing that this means that my custom profile method is getting called on create and returning a NullProfile. How do I do this NullObject properly so that this only happens on read and not on the initial creation of the objects.
I was going exactly through and I wanted a clean new object if it wasn't present(if you're doing this just so object.display doesn't err maybe object.try(:display) is better) this too and this is what I found:
1: alias/alias_method_chain
def profile_with_no_nill
profile_without_no_nill || NullProfile
end
alias_method_chain :profile, :no_nill
But since alias_method_chain is being deprecated, if you're staying on the edge you would have to do the pattern by yourself manually... The answer here seems to provide the better and more elegant solution
2(Simplified/practical version from the answer):
class User < ActiveRecord::Base
has_one :profile
accepts_nested_attributes_for :profile
module ProfileNullObject
def profile
super || NullProfile
end
end
include ProfileNullObject
end
note: The order you do this matter(explained in the linked answer)
On what you tried:
When you did
def profile
#profile || NullProfile
end
It won't behave as expected because the Association is lazily loaded(unless you told it to :include it in the search), so #profile is nil, that's why you're always getting NullProfile
def profile
self.profile || NullProfile
end
It will fail because the method is calling itself, so it's sort like a recursive method, you get SystemStackError: stack level too deep
I've found a simpler option than including a private module in the accepted answer.
You can override the reader method and fetch the associated object using the association method from ActiveRecord.
class User < ApplicationRecord
has_one :profile
def profile
association(:profile).load_target || NullProfile
end
end # class User
Instead of using alias_method_chain, use this:
def profile
self[:profile] || NullProfile.new
end
According to the Rails docs, the association methods are loaded into a module, so it's safe to override them.
So, something like...
def profile
super || NullProfile.new
end
Should work for you.

How to render rails partial from presenter layer?

Well, I'm into this situation as well now using rails 3.2.1
Following is the presenter in app/presenters/form_presenter.rb
class FormPresenter
def render_form
ActionView::Base.new.render partial: "passions/add_form"
end
end
From the view I'm calling,
...
= AddFormPresenter.new.render_form
...
But it blows with the following error:
13:14:12 ActionView::MissingTemplate - Missing partial passions/passion_concept_add_form with {:locale=>[:en], :formats=>[:html, :text, :js, :css, :ics, :csv, :png, :jpeg, :gif, :bmp, :tiff, :mpeg, :xml, :rss, :atom, :yaml, :multipart_form, :url_encoded_form, :json, :pdf, :zip], :handlers=>[:erb, :builder, :slim, :coffee, :rabl]}. Searched in:
...
There is this similar question at RAILS-3.1 render method for ActionView::Base but its not helpful.
How to render this partial from the presenter layer?
Well, I just did it by grabbing the view context using a before filter. My reference was this: https://github.com/amatsuda/active_decorator/blob/master/lib/active_decorator/view_context.rb
So something like:
class FormPresenter
def render_form
FromPresenter.view_context.render partial: "passions/add_form"
end
class << self
def view_context
Thread.current[:view_context]
end
def view_context=(view_context)
Thread.current[:view_context] = view_context
end
end
module Controller
extend ActiveSupport::Concern
included do
before_filter do |controller|
FormPresenter.view_context = controller.view_context
end
end
end
end
and in application_controller.rb
class ApplicationController < ActionController::Base
...
include FormPresenter::Controller
...
end
This isn't typical of the presenter pattern. Presenters are for centralizing complicated data and logic needed to simpify the view's rendering task. Here you are rendering inside the presenter. Is this really what you intend?
Say the answer is yes. Then just creating a new ActionView::Base is asking for trouble because initializing it is non-trivial as shown here. Something strange is going on with class or some other kind of nesting. Where did the passion_concept_ prefix come from in the error message? It looks like you're not telling us all we need about your app.
You may find joy by telling the presenter explicitly where it's rendering:
class FormPresenter
def self.render_form(view)
view.render partial: "passions/add_form"
end
end
Then in the view:
= FormPresenter.render_form(self)
(Here again the explanation is not clear. What is AddFormPresenter?) I don't have a machine where I can try this at the moment, but it ought to be more debuggable than what you've got.

Rails nested attributes callback

Right now I'm working on a Rails app that has an Event model and this model has Category models as nested attributes.
My Event model has a state attribute which must change to certain value if it's nested categories reach a particular amount.
I tried to do this using the after_update callback in the Event model, but it didn't work. Does anyone have any idea?
Why it didn't work? Probably because it reached maximal recursion level.
Try something like this:
class Event < ActiveRecord::Base
attr_accessor :category_count_state_updated
has_many :categories
accepts_nested_attributes_for :categories
attr_accessible :categories_attributes
after_update :update_state
private
def update_state
unless self.category_count_state_updated
self.state = 'categories_count_reached' if self.categories.count == 5
self.category_count_state_updated = true
self.save
end
end
end

Adding the sum of a has_many relationship to a model's attributes

I have the following (simplified model) and wish to access the 'spent' value in the to_json method of the object, ideally as an attribute of the object.
class Task < ActiveRecord::Base
has_many :hours
def spent
self.hours.sum(:spent)
end
end
Is there a way to do this without defining a method and hacking the to_json method? I've been hunting for a way to use scope or by hacking the after_initialize method, but none of these methods provide a 'spent' value when using inspect or to_json on the model.
I need to solve this on models higher up the tree that use a has_many, through relationship too.
You can use the :methods parameter to to_json call.
object.to_json(:methods => :spent)
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
The best solution I could find to this was overriding the 'as_json' method to include the attributes that I needed, or to perform the logic required, (in this case the sum of the has_many relationship).
class Task < ActiveRecord::Base
...
def as_json(options = { })
options = {} if options.nil?
self[:job_id] = self.phase.job_id
self[:spent] = self.hours.sum(:spent)
super(options)
end
end