Re-instancing a class after autoloading with Rails and RSpec - ruby-on-rails

I have this Class where I created some attributes depending on a database table:
class Form
include ActiveAttr::Model
attribute :type, type: String
attribute :default_name, type: String
Languages.all.each do |lang|
attribute :"name_#{lang}", type: String
end
end
This works fine, but then I made two tests with RSpec:
Unit Test:
require 'rails_helper'
RSpec.describe Form, type: :class do
...
end
E2E Test:
require 'rails_helper'
RSpec.describe 'forms', type: :system do
let!(:languages) do
create(:language, name: 'es')
create(:language, name: 'en')
end
scenario 'accessing the page where I can see all the attributes of the Form' do
#form = create(:form, :with_languages)
visit form_page(#form)
end
...
When I run rspec Rails autoloads everything and the Form class is created without any Language in the database yet, so it hasn't any name_ attribute. The first test works fine, but the second one fails because the Form Class was loaded without the Languages mocked:
undefined method `name_en' for #<Form:0x000000014766c4f0>
This is because in order to load the view, we do #view = Form.new(#form) in the controller. And apparently it doesn't create a new object.
If I only run the second test it works like a charm, I tried with DatabaseCleaner but it's the same.
Is there a way to do this without disabling autoloading? I tried disabling it with config.autoload_paths but it gives me thousands of errors and it's a huge app.
I tried several solutions but none of them work, what I need is to re-create that class.

#view = Form.new(#form) does create a new object but it doesn't reload the class, so attributes do not get defined again. attribute is a class method it runs when Form class is loaded, it doesn't run when Form.new is called.
I'm not sure what you're doing with just Form class, usually you'd have a specific class for each form. You could add attributes later, but it is a permanent change to Form class:
>> Form
# Form class is autoloaded and all attributes are defined
Language Load (0.9ms) SELECT "languages".* FROM "languages"
=> Form(default_name: String, type: String)
>> Language.create(name: :en)
>> Form
# will not add language attributes
=> Form(default_name: String, type: String)
>> Form.attribute :name_en, type: String
=> attribute :name_en, :type => String
>> Form
=> Form(default_name: String, name_en: String, type: String)
You could change Form class when initializing a new object:
class Form
include ActiveAttr::Model
attribute :type, type: String
attribute :default_name, type: String
def self.lang_attributes languages = Language.pluck(:name)
Array.wrap(languages).each do |lang|
attribute :"name_#{lang}", type: String
end
end
lang_attributes
def initialize(...)
self.class.lang_attributes
super
end
end
>> Language.create(name: :en)
>> Form # first call to Form autoloads it
Language Pluck (0.8ms) SELECT "languages"."name" FROM "languages"
=> Form(default_name: String, name_en: String, type: String)
>> Language.create(name: :es)
>> Form.lang_attributes
>> Form
=> Form(default_name: String, name_en: String, name_es: String, type: String)
>> Language.create(name: :fr)
>> my_form = Form.new
Language Pluck (0.6ms) SELECT "languages"."name" FROM "languages"
=> #<Form default_name: nil, name_en: nil, name_es: nil, name_fr: nil, type: nil>
# NOTE: but this changes the class itself
If you want the actual class to be dynamic, then the class definition has to be dynamic:
class FormClass
def self.new langs: Language.pluck(:name)
Class.new do
include ActiveAttr::Model
attribute :type, type: String
attribute :default_name, type: String
Array.wrap(langs).each do |lang|
attribute :"name_#{lang}", type: String
end
end
end
end
>> form_class = FormClass.new # aka Form
Language Load (0.5ms) SELECT "languages".* FROM "languages"
=> (default_name: String, name_en: String, name_es: String, name_fr: String, type: String)
>> form_object = form_class.new # aka Form.new
=> #< default_name: nil, name_en: nil, name_es: nil, name_fr: nil, type: nil>
>> Language.destroy_all
>> form_object = FormClass.new.new
Language Load (0.7ms) SELECT "languages".* FROM "languages"
=> #< default_name: nil, type: nil>
# no more language attributes ^
# and you can override language attribute(s)
>> FormClass.new(langs: :en).new
=> #< default_name: nil, name_en: nil, type: nil>

Related

Unable to assign value to an ActiveModel attribute (rails 4.2)

I have created a simple users model in rails 4.2. However I am unable to assign any attribute values in the rails console
2.1.5 :001 > u = User.new
=> #<User id: nil, name: nil, email: nil, auth_token: nil, created_at: nil, updated_at: nil, enabled: true>
2.1.5 :002 > u.name = 'sample'
=> "sample"
2.1.5 :003 > u.changed
=> []
2.1.5 :004 > u
=> #<User id: nil, name: nil, email: nil, auth_token: nil, created_at: nil, updated_at: nil, enabled: true>
As you can see despite setting name the value has not changed.
Here is the model file
class User < ActiveRecord::Base
self.primary_key = :id
include Tokenable
include Creatable
include Updatable
attr_accessor :name, :email, :auth_token, :created_at, :updated_at, :enabled
end
I know that this works fine in rails 3.2
One of the biggest "selling points" of ActiveRecord is that it automatically creates setters and getters in your models based on the DB schema.
These are not just your average accessors created by attr_accessor (which is plain Ruby), they cast values to the correct type and do dirty tracking among other things.
When you use attr_accessor you´re generating setters and getters that clobber those created by ActiveRecord - which means that AR will not track changes or persist the attributes.
This is what you really want:
class User < ActiveRecord::Base
include Tokenable
include Creatable
include Updatable
end
Only use attr_accessor in models when you need setters and getters for non-persisted ("virtual") attributes.
You need to save the record after assigning the new value. You can achieve that by calling update_attribute! or save! on your object. In your case:
u.name = "sample"
u.save!
or
u.update_attribute("name", "sample")
Note that update_attribute updates a single attribute and saves the record without going through the normal validation procedure

Unable to set Rails model attribute from console or controller

I'm new to Rails and am working on getting an application set up in Rails 4.2.4. I have a model called List that looks like the following in the database (PostgresQL):
List(id: integer, user_id: integer, name: string, created_at: datetime, updated_at: datetime, friendly_name: string)
and in List.rb:
class List < ActiveRecord::Base
attr_accessor :name, :friendly_name
belongs_to :user
has_many :items
end
I am trying to modify the name attribute from a controller action:
def save_name
...
list_to_edit = List.find(params[:id].to_i)
list_to_edit.name = params[:name]
list_to_edit.save!
...
end
But the changes are not being persisted. I have confirmed that params[:name] and list_to_edit are not nil. When I try to change the attribute in the Rails console like this:
> l = List.last
> l.name = 'TestName'
> l.save!
I don't see any errors. After executing the above commands and executing l.name I do see TestName. When I type l or List.last, however I still see
#<List id: 29, user_id: 17, name: nil, created_at: "2015-11-07 18:55:04", updated_at: "2015-11-07 18:55:04", friendly_name: nil>
What do I need to do to set the name attribute of a List? I can post any additional file content if it is helpful.
After trying a few more things it looks like all I needed to do was remove name from the array being passed to attr_accessor in List.rb. I believe when I was trying to change the list name with my_list.name = 'something' I was modifying the instance variable, not the attribute stored in the database.

Ruby on Rails model custom to_s method, exclude nill or blank values

I have the following model defined (Using Mongoid, not active record)
class Address
include Mongoid::Document
field :extra, type: String
field :street, type: String
field :area, type: String
field :city, type: String
field :code, type: Integer
validates :street, :city, presence: true
def to_s
"#{extra},#{street},#{area},#{city},#{code}"
end
end
I am defining the to_s method, so i can just use:
<%= address %>
in my views, and it will print out the address correctly. However the issue with the code above, if any of the attributes are blank or nil i end up with the following:
1.9.3p327 :015 > a
=> #<Address _id: 50f2da2c8bffa6e877000002, _type: nil, extra: "hello", street: nil, area: nil, city: nil, code: nil>
1.9.3p327 :016 > puts a
hello,,,,,
=> nil
Using a bunch of unless statements to check if the value is blank or nil seems like the wrong way to go (i can get it to work like that, but seems hackish)
What would be a better way of doing this?
You can use this
def to_s
[extra, street, area, city, code].select{|f| !f.blank?}.join(',')
end
Store elements in an array, throw invalid values out, join with separator.

Rails: Define default values from variable in a new record

I'd like to initiate a new record with some default values.
#test = "#{#fb_page['id']}, slug: #{slug}, title: #{#fb_page['name']}"
#frame = Frame.new(:fb_page_id => #fb_page['id'].to_i, :slug => slug, :title => #fb_page['name'], :theme => 'default')
#test shows all I want:
<the-page-id>, slug: mrs-test, title: Mrs. Test
But a few of the default values for the #frame are nil!
# #frame.inspect:
#<Frame id: nil, slug: nil, title: "Mrs. Test", theme: "default", fb_page_id: nil, created_at: nil, updated_at: nil>
Could anyone please explain me why it doesn't take some of the variables? The string "default" gets through as well as#fb_page['name'].
Thanks in advance!
This typically happens when you don't list those attributes under the attr_accessible section of the model.
attr_accessible :fb_page_id, :slug, :title, :theme
Rails has mass-assignment protection to prevent malicious users from mass-assigning certain model attributes. So when you pass in attributes to a Model.new() method, the only attributes that will persist will be ones that are listed in attr_accessible (unless you haven't protected your models yet).
If you want to leave those attributes protected (as they currently are), you need to set those attributes using dot notation:
#frame = Frame.new
#frame.fb_page_id = #fb_page['id'].to_i
#frame.slug = slug
#frame.title = #fb_page['name']
#frame.theme = 'default'

What is the best practice to get the same behavior from multiple AR objects that are referred to polymorphically?

This is a Rails 3 application.
I have images that can be tied to either a Product or a Brand. A product has an identifier and a Brand has a name.
The polymorphic relationship from Image is called "linkable".
If I want to list the items that a particular image is linked to, I want to avoid doing a conditional in the view like this:
<% for image in Image.all %>
<% if image.linkable.class.name=="Product" %>
<%= image.linkable.identifier %>
<% elsif image.linkable.class.name=="Brand" %>
<%= image.linkable.name %>
<% end %>
<% end %>
Of course I could just put a method inside Brand called "identifier" and use it to call "name". But that's not extensible if i want to add more objects that an image can be linked to. 8 years ago in Java programming I could mandate that a class implemented an Interface, but I don't know if I can do anything like that in Ruby. I appreciate any guidance anybody can offer.
While I like the answer you accepted, let me rewrite your example to be a bit more readable using more idiomatic ruby:
<% Image.each do |image| %>
<%= case image.linkable.class.name
when "Product"
image.linkable.identifier
when "Brand"
image.linkable.name
end %>
<% end %>
You could also easily extract that case statement into a helper function, which might be a good in-between solution if you don't want to create the module and extend it.
in a helper file:
def link_name(image)
case image.linkable.class.name
when "Product"
image.linkable.identifier
when "Brand"
image.linkable.name
end
end
and then your views become:
<% Image.each do |image| %>
<%= link_name(image) %>
<% end %>
You could create a module called Linkable and create the behavior methods in that. Then you extend the module in the classes where you want to add those behaviors. This way you don't have to worry about inheriting from anything you can just mix-in the behavior.
This is the standard Ruby way of adding common functionality to multiple classes without inheriting. You would also, by convention, name your module using a verb based adjective instead of a verb; Linkable vs. Link.
For instance:
module Linkable
def link
puts "Look, I'm linked!"
end
end
class Product < ActiveRecord
extend Linkable
end
class Brand < ActiveRecord
extend Linkable
end
Of course your classes and the module will have actual functionality.
I did it with plain sql
Image.find_by_sql("SELECT * from images INNER JOIN products on (images.linkable_id = products.id AND images.linkable_type = "product");")
Adding the method inside Brand (or Product) is a good way. Since this identifier method represents the contract to the object that an image can be linked to. You can unify it for all, say image_identifier and add this method to all the classes that image links to.
Of course, adding the method to Brand does not only mean defining it inside the class. It can be (rather should) done through a module that is extended by the linkables.
Here is how I tried it:
class Brand < ActiveRecord::Base
extend Linkable
linkable 'identifier'
end
class Product < ActiveRecord::Base
extend Linkable
linkable 'name'
end
class Image < ActiveRecord::Base
belongs_to :linkable, :polymorphic => true
end
module Linkable
def linkable(identifier_name = 'name')
has_many :images, :as => :linkable
instance_eval do
define_method :link_identifier do
send identifier_name.to_sym
end
end
end
end
>> Product
=> Product(id: integer, name: string, created_at: datetime, updated_at: datetime)
>> Brand
=> Brand(id: integer, identifier: string, created_at: datetime, updated_at: datetime)
>> Brand.create :identifier => 'Foo'
=> #<Brand id: 1, identifier: "Foo", created_at: "2010-12-08 16:00:11", updated_at: "2010-12-08 16:00:11">
>> Product.create :name => 'Bar'
=> #<Product id: 1, name: "Bar", created_at: "2010-12-08 16:00:23", updated_at: "2010-12-08 16:00:23">
>> i = Image.new
=> #<Image id: nil, linkable_type: nil, linkable_id: nil, title: nil, created_at: nil, updated_at: nil>
>> i.linkable = Product.first
=> #<Product id: 1, name: "Bar", created_at: "2010-12-08 16:00:23", updated_at: "2010-12-08 16:00:23">
>> i.save
>> i = Image.new
=> #<Image id: nil, linkable_type: nil, linkable_id: nil, title: nil, created_at: nil, updated_at: nil>
>> i.linkable = Brand.first
=> #<Brand id: 1, identifier: "Foo", created_at: "2010-12-08 16:00:11", updated_at: "2010-12-08 16:00:11">
>> i.save
=> true
>> Image.first.link_identifier
=> "Bar"
>> Image.last.link_identifier
=> "Foo"

Resources