I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end
Related
I have the following model:
class TwitterEngagement < ApplicationRecord
end
And I would like to override create (and create!), update (and
update!) methods of it so no one can manually entry fake data. I would like the help of someone more experienced with active record and rails so I don't mess anything up. Right now what I have is:
class TwitterEngagement < ApplicationRecord
belongs_to :page
def create
super(metrics)
end
def update
super(metrics)
end
private
def metrics
client.get_engagements(page.url)
def client
TwitterClient.new
end
end
Thank you.
TL;DR:
class FacebookEngagement < ApplicationRecord
def create_or_update(*args, &block)
super(metrics)
end
Probably depends on your Rails version, but I traced the ActiveRecord::Persistence sometime before in Rails 5, and found out that both create and update eventually calls create_or_update.
Suggestion:
If ever possible, I'll just do a validation, because it kinda makes more sense because you are validating the inputs, and then probably set an optional readonly?, to prevent saving of records. This will also prevent "silent failing" code / behaviour as doing TL;DR above would not throw an exception / populate the validation errors, if say an unsuspecting developer does: facebook_engagement.update(someattr: 'somevalue') as the arguments are gonna basically be ignored because it's instead calling super(metrics), and would then break the principle of least surprise.
So, I'll probably do something like below:
class FacebookEngagement < ApplicationRecord
belongs_to :page
validate :attributes_should_not_be_set_manually
before_save :set_attributes_from_facebook_engagement
# optional
def readonly?
# allows `create`, prevents `update`
persisted?
end
private
def attributes_should_not_be_set_manually
changes.keys.except('page_id').each do |attribute|
errors.add(attribute, 'should not be set manually!')
end
end
def set_attributes_from_facebook_engagement
assign_attributes(metrics)
end
def metrics
# simple memoization to prevent wasteful duplicate requests (or remove if not needed)
#metrics ||= graph.get_object("#{page.url}?fields=engagement")
end
def graph
Koala::Facebook::API.new
end
end
In my blog app, I can call #article.comments.last. How do I create a next_comment method, one that always picks the next comment in line?
Update:
Also, how do I do the reverse, define a previous_comment method?
Update answer below.
Previous record:
class Comment < ActiveRecord::Base
def self.previous(comment, key = :id)
self.where("#{key} < ?", commend.send(key)).first
end
end
In order to define a "next", you must declare a sorting rule. There is no "next" without an order.
The order can be as simple as by primary key, or another field (e.g. a name). The following method should support both cases, and default to id"
class Comment < ActiveRecord::Base
def self.next(comment, key = :id)
self.where("#{key} > ?", commend.send(key)).first
end
end
You should call it on the chain and passing the comment instance, so that it can use the same relation you used to load the original comment
scope = #article.comments
last = scope.last
next = scope.next(last)
Another (maybe simpler) solution is to simply load two objects
current, next = #article.comments.take(2)
You can also make it a method
class Comment < ActiveRecord::Base
def self.first_and_next
# use all to create a scope in case you call
# the method directly on the Comment class
all.take(2)
end
end
current, next = #article.comments.first_and_next(2)
Given that you have a pagination gem, like will_paginate, this will work
# Article model
def next_comment
#page ||= 0
#page += 1
comments.page(#page).per(1).first
end
Or if you don't want to store the state
# Comment model
def next_comment
article.comments.where("id > ?", id).first
end
Dirty solution:
class Comment < ActiveRecord::Base
def next_comment
article.comments.where('id > ?', id).first
end
end
How do you define a method for an attribute of an instance in Ruby?
Let's say we've got a class called HtmlSnippet, which extends ActiveRecord::Base of Rails and has got an attribute content. And, I want to define a method replace_url_to_anchor_tag! for it and get it called in the following way;
html_snippet = HtmlSnippet.find(1)
html_snippet.content = "Link to http://stackoverflow.com"
html_snippet.content.replace_url_to_anchor_tag!
# => "Link to <a href='http://stackoverflow.com'>http://stackoverflow.com</a>"
# app/models/html_snippet.rb
class HtmlSnippet < ActiveRecord::Base
# I expected this bit to do what I want but not
class << #content
def replace_url_to_anchor_tag!
matching = self.match(/(https?:\/\/[\S]+)/)
"<a href='#{matching[0]}'/>#{matching[0]}</a>"
end
end
end
As content is an instance of String class, redefine String class is one option. But I don't feel like to going for it because it overwrites behaviour of all instances of String;
class HtmlSnippet < ActiveRecord::Base
class String
def replace_url_to_anchor_tag!
...
end
end
end
Any suggestions please?
The reason why your code is not working is simple - you are working with #content which is nil in the context of execution (the self is the class, not the instance). So you are basically modifying eigenclass of nil.
So you need to extend the instance of #content when it's set. There are few ways, there is one:
class HtmlSnippet < ActiveRecord::Base
# getter is overrided to extend behaviour of freshly loaded values
def content
value = read_attribute(:content)
decorate_it(value) unless value.respond_to?(:replace_url_to_anchor_tag)
value
end
def content=(value)
dup_value = value.dup
decorate_it(dup_value)
write_attribute(:content, dup_value)
end
private
def decorate_it(value)
class << value
def replace_url_to_anchor_tag
# ...
end
end
end
end
For the sake of simplicity I've ommited the "nil scenario" - you should handle nil values differently. But that's quite simple.
Another thing is that you might ask is why I use dup in the setter. If there is no dup in the code, the behaviour of the following code might be wrong (obviously it depends on your requirements):
x = "something"
s = HtmlSnippet.find(1)
s.content = x
s.content.replace_url_to_anchor_tag # that's ok
x.content.replace_url_to_anchor_tag # that's not ok
Wihtout dup you are extending not only x.content but also original string that you've assigned.
I have a function that does this:
def blank_to_negative(value)
value.is_number? ? value : -1
end
If the value passed is not a number, it converts the value to -1.
I mainly created this function for a certain model, but it doesn't seem appropriate to define this function in any certain model because the scope of applications of this function could obviously extend beyond any one particular model. I'll almost certainly need this function in other models, and probably in views.
What's the most "Rails Way" way to define this function and then use it everywhere, especially in models?
I tried to define it in ApplicationHelper, but it didn't work:
class UserSkill < ActiveRecord::Base
include ApplicationHelper
belongs_to :user
belongs_to :skill
def self.splice_levels(current_proficiency_levels, interest_levels)
Skill.all.reject { |skill| !current_proficiency_levels[skill.id.to_s].is_number? and !interest_levels[skill.id.to_s].is_number? }.collect { |skill| {
:skill_id => skill.id,
:current_proficiency_level => blank_to_negative(current_proficiency_levels[skill.id.to_s]),
:interest_level => blank_to_negative(interest_levels[skill.id.to_s]) }}
end
end
That told me
undefined method `blank_to_negative' for #
I've read that you're "never" supposed to do that kind of thing, anyway, so I'm kind of confused.
if you want to have such a helper method in every class in your project, than you are free to add this as a method to Object or whatever you see fits:
module MyApp
module CoreExtensions
module Object
def blank_to_negative
self.is_number? ? self : -1
end
end
end
end
Object.send :include, MyApp::CoreExtensions::Object
There are a few options:
Monkey-patch the method into ActiveRecord and it will be available across all of your models:
class ActiveRecord::Base
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
Add a "concern" module which you then mix into selected models:
# app/concerns/blank_to_negate.rb
module BlankToNegate
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
# app/models/user_skill.rb
class UserSkill < ActiveRecord::Base
include BlankToNegate
# ...
end
Ruby Datatypes functionality can be extended. They are not sealed. Since you wan to use it in all places why not extend FIXNUM functionality and add a method blank_to_negative to it.
Here's what I ended up doing. I put this code in config/initializers/string_extensions.rb.
class String
def is_number?
true if Float(self) rescue false
end
def negative_if_not_numeric
self.is_number? ? self : -1
end
end
Also, I renamed blank_to_negative to negative_if_not_numeric, since some_string.negative_if_not_numeric makes more sense than some_string.blank_to_negative.
I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])