Conditionally displaying resources in a many-to-many relationship - Rails 4 - ruby-on-rails

Working through my first Rails app. It will be used for searching and viewing data on books within certain categories.
Two resources: Categories and Books. I created a many-to-many HMT (has many through) relationship (following this RailsCast), as each category will have many books and each book will belong to more than one category. Here's the relevant controller code:
Category model (category.rb)
class Category < ActiveRecord::Base
has_ancestry
has_many :categorizations
has_many :books, :through => :categorizations
end
Book model (book.rb)
class Book < ActiveRecord::Base
has_many :categorizations
has_many :categories, :through => :categorizations
accepts_nested_attributes_for :categories
end
Join model (categorization.rb)
class Categorization < ActiveRecord::Base
belongs_to :book
belongs_to :category
end
And here's the database schema:
schema.rb
create_table "books", force: true do |t|
t.string "title"
t.string "url"
t.integer "rank_overall"
t.integer "rank_category"
t.decimal "price"
t.integer "reviews"
t.decimal "rating"
t.date "published"
t.string "img_cover"
t.string "author"
t.datetime "created_at"
t.datetime "updated_at"
t.string "asin"
t.integer "length"
end
create_table "categories", force: true do |t|
t.string "name"
t.text "content"
t.string "ancestry"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "categories", ["ancestry"], name: "index_categories_on_ancestry", using: :btree
create_table "categorizations", force: true do |t|
t.integer "book_id"
t.integer "category_id"
t.integer "rank"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "categorizations", ["book_id"], name: "index_categorizations_on_book_id", using: :btree
add_index "categorizations", ["category_id"], name: "index_categorizations_on_category_id", using: :btree
I have a show view that shows each category. In this view I'd like to show the books that are in each corresponding category. I have these simple loops:
.col-1-3
%strong TITLE
%ul
- #books.each do |book|
%li= book.title
.col-1-3
%strong AUTHOR
%ul
- #books.each do |book|
%li= book.author
.col-1-3
%strong ULR
%ul
- #books.each do |book|
%li= book.url
And in my category model I have the following:
def show
#books = Book.order("title").includes(:categorizations).select("books.*")
end
def book_params
params.require(:book).permit(:books => [{:title => [:book_id]}, {:id => [:category_id]}, :author, :url] )
end
Here's the problem, from the loops in my view I'm receiving information from all the books (ie: titles, authors, and urls of all the books in the database) and I only want that from the books that are in that particular category.
I believe I need to change the logic in my categories_controller.rb, is this correct? I realize that I'm selecting all the books .select("books.*") but I don't know how to edit this to conditionally call only the books that match the category of that being displayed in the show view.
Grateful for insights.
Update:
Realized that this information is also quite helpful. When assigning categories to books (upon book creation) I use this code:
= check_box_tag "book[category_ids][]", category.id, #book.category_ids.include?(category.id), id: dom_id(category)
Perhaps it's possible to somehow reference dom_id(category) inside of the category model?
Forgive my "newbishness." I'm a front-end guy who is only now beginning to really venture into the back-end.

One possible solution is to change the show action in categories like this
#books=Category.find(params[:id]).books
Assuming the :id param is the id for the category (which would be the convention if you are in the categories controller).
If you don't need the information about the category and you only need the books, you could do a join (not a include) with categorizations and use a where like categorizations: {category_id: params[:id]}. That's not as clear as the 1st option, but it saves a query if you are not showing info about the category.

Related

Get value from join model

I'm starting out with Rails (version 5.1.7) and am having some trouble getting my head around the has_many :through associations, specifically how to retrieve values from the intermediary join model.
So I have the following models:
#app/models/project.rb
class Project < ApplicationRecord
belongs_to :owner, class_name: 'User', foreign_key: 'user_id'
has_many :project_users
has_many :users, through: :project_users
end
#app/models/project_user.rb
class ProjectUser < ApplicationRecord
belongs_to :user
belongs_to :project
end
#app/models/user.rb
class User < ApplicationRecord
has_many :project_users
has_many :projects, through: :project_users
has_many :projects, inverse_of: 'owner'
end
The requirements are:
multiple projects and multiple users, with a many-to-many relationship between them;
the relationship needs specific attributes (role, hourly fee, etc), which is why I didn't opt for a HABTM;
each project needs to have a single owner;
both users and owner are items derived from the User model (and a single user may at the same time be a project 'user' and 'owner').
Now here's my database:
#db/schema.rb
ActiveRecord::Schema.define(version: 20210409064744) do
create_table "project_users", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "project_id"
t.string "role"
t.decimal "fee", precision: 10, scale: 2
t.index ["project_id"], name: "index_project_users_on_project_id"
t.index ["user_id"], name: "index_project_users_on_user_id"
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_projects_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "login"
t.string "firstname"
t.string "lastname"
t.string "password_digest"
t.string "email"
t.string "avatar"
t.string "role"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
I have no trouble getting values like <%= #project.name %> or
<strong>Project members:</strong>
<ul>
<% #project.users.each do |proj_user| %>
<li><%= link_to proj_user.name, user_path(proj_user) %></li>
<% end %>
</ul>
in my app/views/projects/show.html.erb.
But how in the world can I get the user's fee for said project? Specifically, how can I retrieve the value of fee from the project_users table?
For clarity, here's the project_users table.
id
created_at
updated_at
user_id
project_id
role
fee
1
2021-04-09 06:54:21.231836
2021-04-09 06:54:21.231836
2
1
member
300
2
2021-04-09 06:54:21.233715
2021-04-09 06:54:21.233715
3
1
member
300
3
2021-04-09 06:54:21.251290
2021-04-09 06:54:21.251290
2
2
member
300
4
2021-04-09 06:54:21.254056
2021-04-09 06:54:21.254056
3
2
member
250
5
2021-04-09 06:54:21.273320
2021-04-09 06:54:21.273320
5
3
member
300
Thanks in advance!
probably better here to change your iteration to iterate through project_users and not users then call users from project_users since it has all the info
<% #project.project_users.each do |proj_user| %>
<li><%= link_to proj_user.user.name, user_path(proj_user.user) %></li>
<% end %>
then you will be able to call proj_user.fee directly
you can also delegate user_name to project_user model like so if you want
class ProjectUser < ApplicationRecord
belongs_to :user
belongs_to :project
delegate :name, to: :user, prefix: true
end
so it would become:
<% #project. project_users.each do |proj_user| %>
<li><%= link_to proj_user.user_name, user_path(proj_user.user) %></li>
<% end %>
you can read more about delegate here https://apidock.com/rails/Module/delegate

Rails 4 HABTM how to set multiple ids in console?

schema.rb:
ActiveRecord::Schema.define(version: 20150324012404) do
create_table "groups", force: :cascade do |t|
t.string "title"
t.integer "teacher_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "groups_students", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "student_id"
end
add_index "groups_students", ["group_id"], name: "index_groups_students_on_group_id"
add_index "groups_students", ["student_id"], name: "index_groups_students_on_student_id"
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "admin", default: false
t.string "type"
t.integer "group_id"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
group.rb:
class Group < ActiveRecord::Base
belongs_to :teacher
has_and_belongs_to_many :students
end
student.rb:
class Student < User
has_and_belongs_to_many :groups
end
I could have set a simple belongs_to and a has_many relationship between the student and group models, but I want students to be able to belong to more than one group, so I set up a HABTM association and corresponding join table.
I think I that right?
The question is, how do I, in the console, set a Student to belong to more than one group?
I have setup a User with 'type: Student' and I have two Groups. So...
In the console I do:
student = Student.first
Then, I want to set 'student' to belong to both Groups, but I don't know how to do this.
To set it to belong to one group I can do:
student.update_attributes(group_id: 1)
But how do make it belong to both groups? It would have two group_id's wouldn't it? I don't know how to set this.
If you need to see any of the other files, it's the 'handcode' branch here:
https://github.com/Yorkshireman/sebcoles/tree/handcode
The answers others have already provided are correct. But if you're working with id's you can also do something like this
student = Student.first
student.group_ids = 1,2,3,4
You don't need to set group_id for the User, the association is handled by the join table and the HABTM statement. You should remove group_id from the users table in the schema.
From memory you should be able to do something like this:
student = Student.first
groups = Group.all
student.groups << groups
student.save
See the active record guide on HABTM associations - specfically 4.4.1.3
Instead of habtm, just use the normal through and your life becomes easy. Make sure an id is generated for the association table (remove id:false)
create_table "group_students", force: :cascade do |t|
t.integer :group_id, nil:false
t.integer :student_id, nil:false
end
class Group < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :group
has_many :students, through :group_students
end
class Student < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :student
has_many :groups, through: :group_students
end
class GroupStudent < ActiveRecord::Base
belongs_to :group,
belongs_to :student
validates_presence_of :group, :student
end
Group.last.students << Student.last
or..
Student.last.groups << Group.last
Student.last.groups = [Group.find(1), Group.find(2)]
etc....
Ok, so it took me 3 days of all kinds of pain to work this out.
There was nothing wrong with my original code, except that I needed to remove the group_id from the user table.
roo's answer was correct, except that using 'group' as a variable name in the console confused Rails. This had led me to believe there was something wrong with my code, but there wasn't. You learn the hard way.
So, Students can be pushed into Groups like this:
To push a student into one group:
student = Student.first
OR
student = Student.find(1)
(or whatever number the id is)
group1 = Group.first
OR
group1 = Group.find(1)
student.groups << group1
To push into multiple groups (which was the original goal of this whole debacle:
student = Student.first
OR
student = Student.find(1)
allclasses = Group.all
student.groups << allclasses
To view your handywork:
student.groups
Works beautifully. The only problem I can see with my code is that it's possible to push the same student into a group twice, resulting in two duplicates of that student in one group. If anyone knows how to prevent this happening, I'm all ears.

Polymorphic Association and extra id field

I need some help with polymorphic associations. Below is my structure:
class Category < ActiveRecord::Base
has_ancestry
has_many :categorisations
end
class Categorisation < ActiveRecord::Base
belongs_to :category
belongs_to :categorisable, polymorphic: true
end
class classifiedAd < ActiveRecord::Base
belongs_to :user
has_one :categorisation, as: :categorisable
end
And here is my schema.rb
create_table "classifiedads", force: true do |t|
t.string "title"
t.text "body"
t.decimal "price"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "classifiedads", ["user_id"], name: "index_classifiedads_on_user_id", using: :btree
create_table "categories", force: true do |t|
t.string "name"
t.string "ancestry"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "categories", ["ancestry"], name: "index_categories_on_ancestry", using: :btree
create_table "categorisations", force: true do |t|
t.integer "category_id"
t.integer "categorisable_id"
t.string "categorisable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
It seems like the associations is correct as when I'm in the console I can do the appropriate commands and all seems to return the right results, for example: Category.first.categorisations or ClassifedAd.first.categorisation. But what I don't understand is saving the association from the Create and editing the record via the Update actions. I'm using simple_form to create my forms and when looking at the params I get the below:
{"title"=>"Tiger",
"body"=>"Huge Helicopter",
"price"=>"550.0",
"categorisation"=>"5"}
and this fails to update or even create as I get this error : Categorisation(#70249667986640) expected, got String(#70249634794540) My controller actions code are below:
def create
#classified = Classifiedad.new(classifiedad_params)
#classified.user = current_user
#classified.save
end
def update
#classified.update(classifiedad_params)
end
def classifiedad_params
params.require(:classifiedad).permit(:title, :body, :price)
end
I think it has something to do with the params as categorisation should be within a sub hash of results, is this right? Also, I need to do something extra within the actions, but what? What the params[:categorisation] value needs to do is save the number into the Categorisations.category_id table column and also save the polymorphic association. This table will be used across other models, which will also be a has_one association, as the user will only be able to select one category for each record. I really hope someone can help me here as the more I look into it the more I get confused :S Please let me know if you ned anymore info from me.
I'm using Rails 4 and Ruby 2
EDIT 2
I managed to get something working but I'm still not sure if its right. Below is the update code for the Create and Update actions. Would be good to know if there is a better way of doing this?
def create
#classified = Classifiedad.new(classifiedad_params)
#classified.user = current_user
**** NEW
cat = Categorisation.new(category_id: params[:classified][:categorisation])
#classified.categorisation = cat
**** END NEW
#classified.save
end
def update
**** NEW
#classified.categorisation.update_attribute(:category_id, params[:classified][:categorisation])
**** END NEW
#classified.update(classifiedad_params)
end

Rails - how to display a name related to an ID

i have a (hopefully) simple question that might have been answered before.. i just couldnt find it... well, here we go, should be easy enough.
I have this schema
create_table "items", :force => true do |t|
t.text "description"
t.string "priority"
t.date "date"
t.time "time"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "done"
t.integer "user_id"
end
create_table "users", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
And I have this in my form for adding a new item:
<%= collection_select(:item, :user_id, User.all, :id, :name) %>
Now, it works, data is saved correctly (I already set up the proper correlations). What i want though, is to display in the items index, the name of the person the item is assigned to, instead of just an ID number.
In items/index I have:
<td><%= item.user_id. %></td>
but i rather want something like
item.user.name
only, it won't work - I guess I need some action in my controller.
Can you help me please? :)
EDIT here is some more details:
My models:
class User < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :user
end
Add this to your Items class:
class Items < ActiveRecord::Base
belongs_to :user
end
Then item.user.name should work.

Need help in troubleshooting association in Rails !

I'm new to rails and and I'm on the urge of learning Associations.
I'm using Rails version 3.
I have a user model and post model.My need is as below:-
Models
class User < ActiveRecord::Base
has_many :post
end
class Post < ActiveRecord::Base
belongs_to :user
validates_associated :user
end
Schema
ActiveRecord::Schema.define(:version => 20101016171256) do
create_table "posts", :force => true do |t|
t.integer "sell_or_buy"
t.string "title"
t.text "body"
t.integer "user_id" <<<<<<< I thought this will help to associate to user model.
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", :force => true do |t|
t.string "name"
t.string "email"
t.string "password"
t.integer "rank"
t.datetime "created_at"
t.datetime "updated_at"
end
end
I thought keeping a user_id field and the belongs_to association will do my job, but
when i tried to display all the posts belonging to a user as follows:
<%= #user.posts %>
in my show.html.erb file. But I get only the following display:-
Name: saran
Email: saran.saran007#gmail.com
Password: abcd
Rank:
Edit | Back
Posts
#<Post:0xb69f47f8>#<Post:0xb69f3024>
I want to display the associated posts "title" and "body" in a readable format.
Also I'm able to create a post with a user_id in which no user exists!. The validates_associated :user is also not working, Please help me out.
Its
class User
has_many :posts
end
Not
has_many :post
Edit and Update your results.
You are getting the posts as expected in your view... So I'm not sure I understand that part of your question. As to the other part, validates_associated just ensures that the attached object is valid itself, and not if it exists at all. For that you want validates_presence_of. See the docs.
I wrote the following partial for my purpose and it works well :).
Thanks for all your inputs.
<% for post in #user.posts do %>
<h3> <%= post.title %> </h3>
<%= post.body %>
<% end %>

Resources