DRYing up my Rails belongs_to and has_many assosiations - ruby-on-rails

I'm working on an app that works out debts and who owes what to whom etc..
Currently it works like this:
create_table "debts", :force => true do |t|
t.string "amount"
t.integer "payer_id"
t.integer "payee_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "people", :force => true do |t|
t.string "name"
t.integer "bank"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
class Debt < ActiveRecord::Base
attr_accessible :amount, :payee_id, :payer_id
belongs_to :payee, :class_name => 'Person'
belongs_to :payer, :class_name => 'Person'
end
class Person < ActiveRecord::Base
attr_accessible :amount, :payee_id, :payer_id
has_many :debts_owed, :class_name => Debt, :foreign_key => "payee_id"
has_many :debts_to_pay, :class_name => Debt, :foreign_key => "payer_id"
end
It's working but I know there must be a simpler way of representing multiple associations to the same model? I've been reading up on has_and_belongs_to_many which looks like the right thing but I'm lost to be honest.
Any help would be appreciated!
Thanks

I don't think a HABTM would help here.
HABTM is for situations where you need a many-to-many relationship, for example, tags. If I have a set of blog posts, I might want to be able to tag them. Blog posts can have many tags, and tags can be used on many blog posts.
That sort of relationship isn't happening in your case. I honestly don't believe there is a better way to set up the associations you have.

For just two lines, that's not too much repetition, BUT if you really wanted to, you could do something like this:
[:payee, :payer].each {|f| belongs_to(f, :class_name => 'Person') }
So that's one line instead of two, but it's probably less clear to read and maintain. It might be worth it if you had a few more lines using the same class_name though.

Related

find by reference

so i have 2 models:
create_table "holders", :force => true do |t|
t.string "faceid"
t.integer "badges_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "holders", ["badges_id"], :name => "index_holders_on_badges_id"
create_table "badges", :force => true do |t|
t.string "name"
t.text "description"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
i need 2 things:
to get all the badges of a certain faceid holder
to get all the holders of a certain badge.
i know its really noobs question but until now i didnt work with references so i dont really understood from the literature how to make the connection.
You actually need a many to many association on your holder and badge models. So you have to options either use has many :through or use has_and_belongs_to_many. The difference between the two can be found here. I am taking the example for has_many :through.
You need to create three models.
class Holder < ActiveRecord:Base
has_many :badges_holders
has_many :badges, :through => :badges_holders
end
class Badge < ActiveRecord:Base
has_many :badges_holders
has_many :holders, :through => :badges_holders
end
class BadgesHolder < ActiveRecord:Base
belongs :badge
belongs :holder
end
And your migration files needs to be:
create_table "holders", :force => true do |t|
t.string "faceid"
t.timestamps
end
add_index "holders", ["badges_id"], :name => "index_holders_on_badges_id"
create_table "badges", :force => true do |t|
t.string "name"
t.text "description"
t.timestamps
end
create_table "badges_holders", :force => true do |t|
t.integer "holder_id"
t.integer "badge_id"
t.timestamps
end
Now you can easily use Holder.find_by_faceid('xyz').badges to find the all hedges held by the holder whose faced is xyz. And Badge.first.holders to get all the holders for the first bedge.
For your question HABTM will be a good option as you do not need any extra field in the join table, so you can just use has_and_belongs_to_many in both of your models and you don't need BadgesHolder model in that case. And for the migration of the join table, replace first line with create_table "badges_holders", :id => false, :force => true do |t| a and remove t.timestamps as the join table for HABTM should not have any other column than the foreign keys.
If it's some Ruby on Rails, you must have 2 models :
class Holder < ActiveRecord:Base
has_many :badges
end
class Badge < ActiveRecord:Base
belongs_to :holder
end
Your entry called badges_id should not be in your holders table ; you should have a holder_id on your "badges" table.
Then, you can simply call
Holder.find_by_faceid('foobar').badges
and
Badge.find(1337).holder
If your badge can belongs to many holders, then you have to write a has_and_belongs_to_many relation.

rails self join

I want a user to be able to create a challenge (challenges_created) and other users to be able to offer support to achieve them (challenges_supported). I tried to do this with a self joined challenge model with its resources nested beneath the users resource. I currently have models:
class User < ActiveRecord::Base
attr_accessible :name, :supporter_id, :challenger_id
has_many :challenges_created, :class_name => 'Challenge', :foreign_key => :challenger_id
has_many :challenges_supported, :class_name => 'Challenge', :foreign_key => :supporter_id
end
and
class Challenge < ActiveRecord::Base
attr_accessible :challenger, :completion_date, :description, :duration, :status, :supporter, :title
belongs_to :challenger, :class_name => 'User'
has_many :supporters, :class_name => 'User'
end
I think that I would need full CRUD and corresponding views both for when users are creating challenges and when they are supporting them. Because of this, I created 2 controllers named challenges_created_controller and challenges_supported_controller.
My routes.rb file is:
resources :users do
resources :challenges_created
resources :challenges_supported
end
The problem that I am encountering with this setup is that when I try to create a new challenge at
http://localhost:3000/users/3/challenges_created/new
I receive the message
Showing /home/james/Code/Rails/test_models/app/views/challenges_created/_form.html.erb where line #1 raised:
undefined method `user_challenges_path' for #<# <Class:0x007fb154de09d8>:0x007fb1500c0f90>
Extracted source (around line #1):
1: <%= form_for [#user, #challenge] do |f| %>
2: <% if #challenge.errors.any? %>
The result is the same for the edit action too. I have tried many things but if I were to reference #challenge_created in the form_for then it is not matching the Challenge model.
Can anybody please advise on how what I am doing wrong. Thank you in advance. My schema is:
create_table "users", :force => true do |t|
t.string "name"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "challenger_id"
t.integer "supporter_id"
end
create_table "challenges", :force => true do |t|
t.string "title"
t.text "description"
t.integer "duration"
t.date "completion_date"
t.string "status"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "challenger_id"
t.integer "supporter_id"
end
I think the problem is that you have a challenge_created controller, but you do not have model for it. In you form you specify a user and a challenge so rails tries to find a controller for challenge, not challenge_created. Rails thinks that for a model you have a controller named based on the convention.
I recommend you not to create two different controllers for challenges. Use only one and differenciate on actions. E.g. ou can create a list_created and list_supported action in challenges.

Proper structure for a join table in Rails 3.2

I am rewriting a PHP based support ticket system in Rails and have run into a snag.
I have my users table created and my tickets table created
create_table "tickets", :force => true do |t|
t.integer "user_id", :null => false
t.integer "department_id", :null => false
t.integer "upload_id", :null => false
t.string "subject", :null => false
t.text "body", :null => false
t.string "status_id", :null => false
t.text "url", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "users", :force => true do |t|
t.string "fName", :null => false
t.string "lName", :null => false
t.string "seKey", :null => false
t.boolean "isAdmin"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.string "auth_token"
end
Everything is working great, I am able to create tickets, etc...
Now I am needing to assign 1 or more admins to a ticket and am not sure if I should use a has_and_belongs_to_many relationship or a has_many :through relationship.
The way it is currently setup in PHP is just using a join table that matches a userID with a ticketID. I don't think I will ever need any other data relating to the relationship other than that so is has_and_belongs_to_many the best option?
Also, will this cause an issue since the ticket is already associated with a record in the user table (the original creator)? Basically a ticket will have multiple connections to the users table, one will be the person who submitted it and the rest are users who are assigned to handle it.
Both work; however, personally, I prefer a has_many :through relationship. The reason being is you have access to the join table. To do this generate a model called ticket_user with a field called user_id and ticket_id. Then in the model add
belongs_to :user
belongs_to :ticket
then you can add to the ticket model
has_many :ticket_users
has_many :users :through ticket_user
and this to the user model
has_many :ticket_users
has_many :ticket :through ticket_user
Then to retrieve all of a users tickets do
user.tickets
To get all of a tickets users do
ticket.users
For more info check out this guide
this may work - create two relationships between user and ticket, ticket belongs_to creator, :class_name user and ticket has_many assigned_users, class_name user and in user model has_many created_tickets and has_many assigned_tickets
In your case has_and_belongs_to_many is the extra relationship.
has_and_belongs_to_many :members, :class_name => "User", :join_table => "members_tickets"
You can have to create one table members_users with ticket_id and member_id as fields as shown in http://guides.rubyonrails.org/association_basics.html#choosing-between-has_many-through-and-has_and_belongs_to_many
now you can get list of assigned members like below
#ticket.members # list of members
#ticket.members << #user # add new member

Rails - How to associate models in one direction only

Ahoy guys,
I'm new to Rails, and I feel like I'm definitely missing something crucial here, because it seems like this should be an easily solvable problem.
I've set up a Page model and a Coord model (with help from the getting started tutorial), and Coord successfully belongs_to Page. I'm trying to apply similar logic to make another model, Comment, belong to Coord, and only belong to Page via Coord.
Do I use :through for an association that (I think) only needs to link in one direction? As in Page < Coord < Comment?
At the moment I have:
class Page < ActiveRecord::Base
attr_accessible :description, :name
has_many :coords
has_many :comments, :through => :coords
end
Coord model:
class Coord < ActiveRecord::Base
belongs_to :page
has_many :comments
attr_accessible :coordinates, :x, :y
validates :x, :presence => true
validates :y, :presence => true
end
Then the Comment model:
class Comment < ActiveRecord::Base
belongs_to :coord
belongs_to :page
attr_accessible :body
end
I still keep getting errors about comments being an undefined method, or an association not being defined. Apologies if this is a common question, I don't personally know anyone who knows Rails, and the documentation only has examples too far removed from mine (to my knowledge). Thanks
Edit: added DB schema
ActiveRecord::Schema.define(:version => 20120712170243) do
create_table "comments", :force => true do |t|
t.text "body"
t.integer "coord_id"
t.integer "page_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "comments", ["coord_id"], :name => "index_comments_on_coord_id"
add_index "comments", ["page_id"], :name => "index_comments_on_page_id"
create_table "coords", :force => true do |t|
t.string "coordinates"
t.integer "x"
t.integer "y"
t.integer "page_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "coords", ["page_id"], :name => "index_coords_on_page_id"
create_table "pages", :force => true do |t|
t.string "name"
t.string "description"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end
Page
class Page < ActiveRecord::Base
has_many :coords
has_many :comments, :through => :coords
end
Coord
class Coord < ActiveRecord::Base
belongs_to :page
has_many :comments
end
Comment
class Comment < ActiveRecord::Base
belongs_to :coord
has_one :page, :through => :coord
end
Using the above, you don't need page_id in the comments table.
Reference: A Guide to Active Record Associations

Using build with has_many :through

I have an Entry model and a Category model, where an Entry can have many Categories (through EntryCategories):
class Entry < ActiveRecord::Base
belongs_to :journal
has_many :entry_categories
has_many :categories, :through => :entry_categories
end
class Category < ActiveRecord::Base
has_many :entry_categories, :dependent => :destroy
has_many :entries, :through => :entry_categories
end
class EntryCategory < ActiveRecord::Base
belongs_to :category
belongs_to :entry
end
When creating a new Entry, I create it by calling #journal.entries.build(entry_params), where entry_params is the parameters from the entry form. If any categories are selected, however, I get this error:
ActiveRecord::HasManyThroughCantDissociateNewRecords in Admin/entriesController#create
Cannot dissociate new records through 'Entry#entry_categories' on '#'. Both records must have an id in order to delete the has_many :through record associating them.
Note that the '#' on the second line is verbatim; it doesn't output an object.
I have tried naming my categories selectbox on the form to categories and category_ids but neither make a difference; if either is in the entry_params, the save will fail. If no categories are selected, or I remove categories from entry_params (#entry_attrs.delete(:category_ids)), the save works properly, but the categories don't save, obviously.
It seems to me that the problem is that an EntryCategory record is attempting to be made before the Entry record is saved? Shouldn't build be taking care of that?
Update:
Here's the relevant parts of schema.rb, as requested:
ActiveRecord::Schema.define(:version => 20090516204736) do
create_table "categories", :force => true do |t|
t.integer "journal_id", :null => false
t.string "name", :limit => 200, :null => false
t.integer "parent_id"
t.integer "lft"
t.integer "rgt"
end
add_index "categories", ["journal_id", "parent_id", "name"], :name => "index_categories_on_journal_id_and_parent_id_and_name", :unique => true
create_table "entries", :force => true do |t|
t.integer "journal_id", :null => false
t.string "title", :null => false
t.string "permaname", :limit => 60, :null => false
t.text "raw_body", :limit => 2147483647
t.datetime "created_at", :null => false
t.datetime "posted_at"
t.datetime "updated_at", :null => false
end
create_table "entry_categories", :force => true do |t|
t.integer "entry_id", :null => false
t.integer "category_id", :null => false
end
add_index "entry_categories", ["entry_id", "category_id"], :name => "index_entry_categories_on_entry_id_and_category_id", :unique => true
end
Also, saving an entry with categories works fine in the update action (by calling #entry.attributes = entry_params), so it does seem to me that the problem is only based on the Entry not existing at the point that the EntryCategory records are attempted to be created.
I tracked down the cause of this error to be within the nested_has_many_through plugin. It seems that the version I had installed was buggy; after updating to the most recent version, my build works again.
Why do you call
self.journal.build(entry_params)
instead of
Entry.new(entry_params)
If you need to create a new entry associated to a specific Journal, given a #journal, you can do
#yournal.entries.build(entry_params)

Resources