Normally one would build a related model instance through its parent object:
#child = #parent.children.build(params[:child])
But when we're using STI and we want to do this while at the same time building it as one of the subclasses the syntax seems to break down. This is the best way to do it as far as I can tell (ignore the security problems around not checking the type against an approved list):
#child = params[:type].classify.constantize.new(params[params[:type]])
#child.parent = #parent
Is this the best way to go about it? I'm using a single controller to build all the different subclass types so I need to supply the type as a parameter.
I usually use this trick:
class BaseModel < ActiveRecord::Base
private
# this will enable you to set the type field
def attributes_protected_by_default
super - [self.class.inheritance_column]
end
end
Now in your controller:
#parent.children.build(params[:base_model])
Make sure params[:base_model] hash has a field called type. I usually add a hidden form field to store the type.
Make sure you add the relevant checks to ensure the correct sub types are created.
Note : This trick will not work in Rails 3.
Related
In rails, we can simply define default attribute value in model level with one macro statement from gem 'default_value_for' or by ourself. On the other side, we can define it in the database level with migration 'default' option. I am confused about which is the best practice of rails, or which way should we used in different scenarios?
Any reply is appreciate! :D
Things you might want to consider.
When you set the default in the application then
the default can be changed easily and
the default can be dynamic and
there are ways to bypass the default
When you set the default in the database then
changing the default needs a migration (what might be a problem for huge tables)
it is harder to implement dynamic defaults and then
there is no way to bypass the default in the application (more secure)
Another option you might want to consider is to override the attributes getter method. This approach only uses the default value when an empty (or invalid value) is returned from the database, it doesn't change the way of storing new values.
def foo
super || 'a default value'
end
Setting defaults in the database is generally preferable as ActiveRecord is built around a convention over configuration approach where the database table drives the model - and not the other way around.
However database defaults are not very smart - they will always be applied and always have the same static value* (well unless you change the DB schema). In most cases this does not matter.
However if the default value requires some sort of computation like for example setting the default country of a user by IP based geolocation you would need to set the defaults on the application level (model).
Other examples would be:
columns that store serialized data (not native JSON columns)
relations that should default to a certain record.
Where do you set default values in the application?
in the model
The simplest example is using model callbacks:
class Thing
after_initialize :set_defaults, if: :new_record?
private
def set_defaults
self.foo = 'bar'
end
end
The con of model callbacks is that it can be very difficult to control exactly where in your application flow it is happing. For example if the initialization block is expensive you don't want it happening in all your tests.
In the controller
If the default value relies on the context of the request such as this example which uses Geocoder for IP based geolocation:
class User < ActiveRecord::Base
def set_default_location(geo)
u.city = geo.city
u.zipcode = geo.postal_code
u.country = geo.country_code
end
end
class UserController
def new
#user = User.new do |u|
u.set_default_location(request.safe_location)
end
end
end
The con is that this can lead to bloated controllers if not carefully kept in check.
In a PORO / Factory.
Some would argue that placing too much of your business logic inside your ORM classes (subclasses of ActiveRecord::Base) leads to violation of the Single Responsiblity Principle and makes your application overly brittle.
module ManagerFactory
def self.new(attributes = {})
user = User.new(attributes)
user.add_role(:manager)
user
end
end
I would say when to use it and when not to use it based on the data you want to set as default.
If what you have in mind is a static value to be set as default, example: false, 0, user, if you something dynamic in mind to be set as default value. Life a randomly generated UUID or a date which changed with respect to the day of creation then you can use default_value_for gem.
I have a model called Coupons
Then I have two child models CouponApplicationsand ApprovedCoupons.
The last two inherit from Couponsvia an STI architecture.
Now I want to realise the following:
A user sees CouponApplications
He clicks on an Approve button which makes the CouponApplications ApprovedCoupons
I realise that I could simply update the typecolumn of the Couponsrecord to change types. However, there are several Concerns, hooks etc in the ApprovedCoupons model which are happening after creation so this is not so easy. In fact, I want to create a complete new record to trigger those Concerns, hooks etc.
So I wrote this which I consider really bad:
#coupon_application = CouponApplication.find(params[:id])
#approved_coupon = ApprovedCoupon.new
# copy/paste attributes except the ID as this would be considered a duplication
#approved_coupon.attributes = #coupon_application.attributes.except("id")
# set the new type
#approved_coupon.update_attributes(type: "Advertisement")
#approved_coupon.save
I hope you understand what I want to achieve. It works this way but I doubt this is clean code.
To summarize:
I want to change the Coupontype from CouponApplication to
ApprovedCoupon
I still want to trigger the Concerns, hooks etc. in my ApprovedCoupon model so I decided to create a new ApprovedCoupon
record instead of just changing the type.
Is there any better approach?
You could add an approve method to your CouponApplication model like this:
class CouponApplication < Coupon
...
def approve
data = attributes.except('id', 'created_at', 'updated_at')
ApprovedCoupon.create(data.merge(type: 'Advertisement'))
end
end
Now your code can be simplified to this:
#coupon_application = CouponApplication.find(params[:id])
#approved_coupon = #coupon_application.approve
I am using ROAR to implement an API for a rails application. This application deals with tickets that can have attributes like a subject and a description, but also have user defined attributes. For simplicity lets assume a ticket looks like:
class Ticket
attr_accessor :subject, :description
def custom_attributes
# in reality these attributes depend on the current ticket instance
# they are not hard-coded into the class
[['priority', 'high'], ['Operating System', 'Ubuntu']]
end
end
The desired JSON output for such a ticket looks as follows:
{
"subject": "Foo",
"description": "Bar",
"customField1": "high",
"customField2": "Ubuntu"
}
Now you might already see the problem. All properties are immediate children of the root object, this means I can't write that up as representer:
class TicketRepresenter
property :subject
property :description
# Need to iterate over instance members on the class level here...
end
Is there some mechanic that ROAR offers to accomplish that? E.g. a callback that is executed in the context of an actual instance, e.g.
def call_me_on_write
represented.custom_attributes.each do |attribute|
add_property('customField1', attribute[1])
end
end
Is there something like this in ROAR that I have overlooked to accomplish this?
I looked in both the docs for ROAR and the docs for representable, but could not find anything.
Disclaimer
I tried to simplify the actual circumstances to make the question more readable. If you think that important information are missing, please tell me. I will thankfully provide more details.
Out of scope
Please do not discuss whether the chosen JSON format is a good/bad idea, I want to evaluate whether ROAR would support it.
I believe the best approach for the problem would be to use Roar's writer:. It completely turns control over the output to you by passing a handful of values it calls options to a provided lambda.
For example:
property :desired_property_name, writer: -> (represented:, doc:, **) do
doc[:desired_key] = represented.desired_value
end
There are a lot of uses not covered by the github readme but which are documented on the Trailblazer website. This one in particular can be found at http://trailblazer.to/gems/representable/3.0/function-api.html#writer.
Cheers!
I ended up dynamically creating classes from my basic representer:
class TicketRepresenter
property :subject
property :description
def self.create(ticket, context = {})
klass = Class.new(TicketRepresenter) # create a subclass of my representer
ticket.custom_attributes.each do |attribute|
# for each custom attribute in the actual instance insert a property into the created class
property "customField#{attribute.id}".to_sym
getter: -> (*) { attribute.value }
end
# return an instance of the class created above
klass.new(ticket, context)
end
end
Basically that means the actual representer class used to create the JSON is a different one for each Ticket.
If you wanted to read a Ticket back from JSON, it is neccessary to correctly initialize the representer so that the created representer class knows about your custom fields and also define setters.
You will now need to conventionally call the new create method instead of new.
If you need your representer to be created by ROAR (e.g. for a collection), you can use the Polymorphic Object Creation mechanism of ROAR.
Note: The code above does not exactly fit the example of custom attributes posted in my question, but you should get the idea (in the example an attribute did not have members like id and value, but was list consisting of key and value).
I have a model that defines methods based off of the entries in another model's table: eg Article and Type. An article habtm types and vice versa.
I define in Article.rb:
Type.all.each do |type|
define_method "#{type.name}?" do
is?(:"#{type.name}")
end
end
This works great! it allows me to ensure that any types in the type db result in the methods associated being created, such as:
article.type?
However, these methods only run when you load the Article model. This introduces certain caveats: for example, in Rails Console, if I create a new Type, its method article.type_name? won't be defined until I reload! everything.
Additionally, the same problem exists in test/rspec: if I create a certain number of types, their associated methods won't exist yet. And in rspec, I don't know how to reload the User model.
Does anyone know a solution here? Perhaps, is there some way to, on creation of a new Type, to reload the Article model's methods? This sounds unlikely.. Any advice or guidance would be great!
I think you'll be better off avoiding reloading the model and changing your api a bit. In Article, are you really opposed to a single point of access through a more generic method?
def type?(type)
return is? type if type.is_a? String # for when type is the Type name already
is? type.name # for when an instance of Type is passed
end
If you're set on having separate methods for each type, perhaps something like this would work in your Type class
after_insert do
block = eval <<-END.gsub(/^ {6}/, '')
Proc.new { is? :#{self.name} }
END
Article.send(:define_method, "#{self.name}?", block)
end
I am trying to use single table inheritance for some of my models. The base model is a Tournament, and I wish to extend this to create different types of tournaments. For instance, I might want to add a SingleEliminationTournament, or a DoubleEliminationTournament, both of which would inherit from Tournament. I have 2 questions, both of them somewhat related.
1) I would like the user to be able to create tournaments with a form, and to do this they would need to select one of the subclasses. Is there a way to get all of the subclasses and use them to populate a select box or something like that?
2) Since this information is going into a form, it would be nice to be able to validate the input into type. To do this, I would like to add a validation in the Tournament class that could check to make sure the Type was valid.
Obviously, I could hard code the values into the validation and the form, but I would not like to do that. Any help would be appreciated. Thanks!
TheModel.subclasses
would give you a list of types you need to include, but only if the models are loaded at runtime. They will always be loaded in production mode. You will have to load them manually in development mode.
You could create a directory with tournaments in them and load them with Dir.glob('app/tournaments/**/*_tournament.rb'). This gives you a nice listing all the tournament files you've specified. Because of convention, you can then infer the proper class name for each tournament.
Store this list of tournament names somewhere for reference in you validations and forms.
I'm not a Rails expert and I'm not sure if this can be considered clean, but for the validation part of your question, this worked for me:
Inside Tournament model:
def validate_type_implemented
klass = type.constantize rescue Object
raise "Given type not available." unless klass.class == Class and klass <= self.class
end