Rails - extend attributes method - ruby-on-rails

I have a group of input, for which I expect a large amount data (list of objects), so I want this input on create/update action to be wrapped inside of ActiveRecord transaction.
There is model Student, which has_one Account.
ACCOUNT_FIELDS=[:name, :surname, :email, :phone, :activated]
has_one :account, :as => :account_holder, autosave: true, :dependent => :destroy
validates_associated_extended :account
ACCOUNT_FIELDS.each do |action|
define_method action do
get_or_build_account.send(action)
end
setter_action = action.to_s + "="
define_method setter_action do |arg|
get_or_build_account.send(setter_action, arg)
end
end
here I made a reader/writer methods, so #student.name will return related data from account, also I can assign it through #student thanks to autosave.
Issue: as I said, I want it to be wrapped inside of transaction, so in my controller I don't save anything. Each student is assigned like this
student.attributes = #...data
Where student later on passed to transaction block.
But! For this specific model I want to student.attributes also return fields from ACCOUNT_FIELDS.
Normally it works with student.account_attributes but as I said, later student is processed in transaction, and it is made with module, which I want to be reusable for some other models (which doesn't need this logic).
So rather than modifying my module code with some conditions, I want instance of this model to return needed account fields when just called self.attributes
#student.attributes #=> :school_id => 1, :name => "John"...
where name is self.name from self.account.name

Try this:
def attributes
new_attributes={}
ACCOUNT_FIELDS.each do |field|
new_attributes[field]=self.send(field)
end
super.merge(new_attributes)
end

Related

Uniqueness error in has_many nested attributes

I have a class student with has_many tests. The test class has a student_id, marks, name. Here the test name should be unique. The test is a nested attribute for student. So the parameters are this way:
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {:name => "bgc", :marks => "470"}}}
I have a problem with update. If I update_attributes with the tests_attributes, it throws a validation error saying the name for test is not unique. I am actually addressing the same record here. How do I overcome this?
Without seeing your models (& validations), it's going to be quite difficult to diagnose your error directly.
--
Nested Attributes
We've done something like this, and found that your nested data is passed to the child model as if it were receiving a new object (without being nested). This means if you've got validates uniqueness for that model, it should be okay:
#app/models/test.rb
Class Test < ActiveRecord::Base
belongs_to :student
validates :name, uniqueness: true
end
Reason I write this is because there's a method called inverse_of, which basically allows you to access the parent model data in your child model
--
Update
I think the problem will likely lie with your use of update_attributes. Problem being you're trying to update both the student and the test attributes at one time.
I'm not sure exactly why this would be a problem, but I'd test this:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.test.update(name: params[:test_name], marks: params[:marks])
end
end
I think if you can explain your methodology a little more, it will be much more helpful. I.E are you trying to update student or test? If you're updating student & adding a new test, how are you updating the studet?
Thanks for the reply guys. I ended up finding the answer myself. I did have a uniqueness validation for name.
I had a situation where initially I wouldn't know the student but have only his details. So I would have to create this hash and pass it to update. The trick to not trying to create a new record for the same name in test is to pass the actual record's ID along with it. This solved the problem
Nested Attributes
I think the problem with nested_attributes. For update need to pass nested_attributes with ID.
Ex.
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {id: 1, :name => "bgc", :marks => "470"}}}
I have tried below-given example it is worked for me:
Update
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.update_attributes(student_params)
end
private
def student_params
params.require(:student).permit(:first_name, :email,
tests_attributes: [:id, :name, :marks])
end
end

Using a method within model, calling it from view

I have an Update model which belongs to users.
To show all of one user's friends' Updates, I am doing something like:
Update.where("user_id" => [array_of_friend_ids])
I know the "right" way of doing things is to create a method to create the above array. I started writing the method but it's only half-working. Currently I have this in my user model:
def self.findfriends(id)
#friendarray = []
#registered_friends = Friend.where("user_id" => id)
#registered_friends.each do |x|
#friendarray << x.friend_id
end
return #friendarray
end
I am doing the entire action in the view with:
<% #friendinsert = User.findfriends(current_user.id) %>
<% #friendarray = [] %>
<% #friendarray << #friendinsert %>
<%= #friendarray.flatten! %>
Then I'm calling Update.where("user_id" => #friendarray) which works. But obviously I'm doing things in a very hacky way here. I'm a bit confused as to when Rails can "see" certain variables from models and methods in the view. What's the best way to go about inserting an array of IDs to find their Updates, since I'm not supposed to use much logic in the view itself?
Mattharick is right about using associations. You should use associations for the question you mentioned in description of your question. If we come to the question at the title of your question;
let's say you have a User model.
These two methods are different:
def self.testing
puts "I'm testing"
end
and the other one is:
def testing
puts "I'm testing"
end
Pay attention to the self keyword. self keyword makes method a Class method. Which you can call it from your controllers or views like: User.testing.
But the one with out testing is a instance method. Which can be called like:
u = User.last
u.testing
Second one gives you possibility to use attributes of the 'instance' inside your model.
For example, you can show name of your instance in that method just like this?
def testing
puts "Look, I'm showing this instance's name which is: #{name}"
end
These are powerful stuff.
Practise on them.
Simple add another association to your project.
class User < ActiveRecord::Base
has_many :friendship
has_many :friends, :through => :friendship, :class_name => User, :foreign_key => :friend_id
has_many :friendship
has_many :users, :through => :friendship
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => User
end
I don't know if my synrax is correct, please try out.
Friendship has the attributes user_id and friend_id.
After that you should be able to do something like following to get the updates of a friend:
User.last.friends.last.updates
You can work with normal active record queries instead of hacky arrays..

ActiveRecord RoR - Saving only new associated objects

How can I save (insert) only associated objects without saving (updating) the base object?
For example I just want to save the phone numbers, I don't wan to resave/update the person object.
def create_numbers
#params => person_id => 41, person => {:phone_number => '12343445, 1234566, 234886'}
#person = params[:person_id]
nums = params[:person][:phone_numbers].split(',')
nums.each do |num|
#person.phone_numbers.build(:number => num)
end
#person.save #here I just want to save the numbers, I don't want to save the person. It has read only attributes
end
Models:
Person < ...
# id, name
belongs_to :school, :class_name => :facility
has_many :phone_numbers
end
PhoneNumber < ...
# id, number
belongs_to :person
end
This is a bit of a dumb example, but it illustrates what I'm trying to accomplish
How about #person.phone_numbers.create(:number => num)
The downside is that you wont know whether it failed or not - you can handle that, but it depends on how exactly you want to handle it.
The simplest approach is to replace your build(:number => num) with create(:number => num), which will build and save the phone_number object immediately (assuming it passes validation).
If you need to save them all after creating the whole set (for some reason), you could just do something like
#person.phone_numbers.each{|num| num.save}

Mongoid validation uniqueness with scope and belongs_to

I have the following mongoid model, with a scoped validation to prevent multiple votes on one bill. Each vote belongs to a user and a group:
class Vote
include Mongoid::Document
field :value, :type => Symbol # can be :aye, :nay, :abstain
field :type, :type => Symbol # TODO can delete?
belongs_to :user
belongs_to :polco_group
embedded_in :bill
validates_uniqueness_of :value, :scope => [:polco_group_id, :user_id, :type]
end
The user has the following method to add a vote to a bill:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
bill.votes.create(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
and a Bill class which embeds many :votes. This is designed to allow a user to associate their vote with different groups ("Ruby Coders", "Women", etc.) and is working well, except the database currently allows a user to vote multiple times on one bill. How can I get the following to work?
u = User.last
b = Bill.last
u.vote_on(b,:nay)
u.vote_on(b,:nay) -> should return a validation error
Most probably validators on Vote are not getting fired. You can confirm that by adding a validates function and outputting something or raising an exception in it.
class Vote
validate :dummy_validator_to_confirmation
def dummy_validator_to_confirmation
raise "What the hell, it is being called, then why my validations are not working?"
end
end
If after creating above validations User#vote_on doesn't raises exception, it confirms that callbacks are not fired for Vote via vote_on method. You need to change your code to fire callbacks on Vote. Probably changing it to resemble following would help:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
vote = bill.votes.new(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
vote.save
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
There is an open issue on mongoid github issue tracker to allow cascade callbacks to embedded documents. Right now callbacks are only fired on document on which persistence actions are taking place on.

I feel like this needs to be refactored - any help? Ruby modeling

So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.

Resources