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
Related
I am trying to make an association that is not working.
I have the following scope:
ActiveRecord::Schema.define(version: 2020_04_05_125812) do
create_table "accounts", force: :cascade do |t|
t.string "social_network"
t.string "name_account"
t.integer "person_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["person_id"], name: "index_accounts_on_person_id"
end
create_table "lists", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "lists_people", id: false, force: :cascade do |t|
t.integer "list_id", null: false
t.integer "person_id", null: false
end
create_table "people", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "posts", force: :cascade do |t|
t.string "post_text"
t.date "date"
t.string "link"
t.integer "account_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_posts_on_account_id"
end
add_foreign_key "accounts", "people"
add_foreign_key "posts", "accounts"
end
I wish I could consult, for example:
I want to consult the person named "Test_name"
That person belongs to lists, which is a collection of people. In addition, that person has accounts and those accounts have Posts.
class List < ApplicationRecord
has_and_belongs_to_many :people
end
class Person < ApplicationRecord
has_and_belongs_to_many :lists
end
class Account < ApplicationRecord
has_many :posts
belongs_to :person
end
class Post < ApplicationRecord
belongs_to :account
end
How could I have a return like the one below:
List | Name | social_network
1 | Test_name | facebook
2 | Test_name | twitter
All the queries I make, either return the wrong type, or return only the list.
Problems such as:
List | Name | social_network
1 | Test_name | facebook
2 | Test_name | twitter
1 | Second_name | twitter
And I don't want to see the data "second_name"
I try this:
#lists = List.from(
Person.left_outer_joins(:list).where('people.name LIKE ?', "Renata Rempel"),
:list
)
but, doesn't works =/
To start off with you want to setup a many to many association between Person and List. This can be done with has_and_belongs_to_many but there are many reasons why has_many through: may be a better choice. The primary one is that it will let you add features like keeping track of banned users or when a user joined a list.
# rails g model list_membership member:belongs_to user:belongs_to
class ListMembership < ApplicationRecord
belongs_to :member, class_name: 'Person'
belongs_to :list
end
We then have to fix the foreign key in the association:
class CreateListMemberships < ActiveRecord::Migration[6.0]
def change
create_table :list_memberships do |t|
t.belongs_to :list, null: false, foreign_key: true
t.belongs_to :member, null: false, foreign_key: { to_table: :people }
t.timestamps
end
# can be a good idea to add a compound index
# add_index [:list_id, :member_id], unique: true
end
end
class Person < ApplicationRecord
has_many :list_memberships, foreign_key: :member_id
has_many :lists, through: :list_memberships
has_many :accounts
has_many :posts, through: :accounts
end
class List
has_many :list_memberships
has_many :members,
through: :list_memberships
end
Your from query will not work as your actually selecting rows from people but you just alias the table lists. That won't magically select the right data! If you really wanted to use from you would do:
List.from(
List.joins(:members).where("people.name LIKE ?", "Renata Rempel"),
:lists
).eager_load(members: :posts)
If you want to create a bunch of lists with a random number of members in your seed file you can just do:
ids = 10.times.map do
Person.create!(name: Faker::Name.name).id
end
lists = 10.times.do
List.create!(member_ids: ids.sample(2))
end
I'm looking to be able to call the values from another table this way in the view.
<%= #team.r1.players.full_name %>
<%= #team.r2.players.full_name %>
...
<%= #team.r2.players.full_name %>
r1 to r10 are columns that hold player IDs.
The models are pretty simple and look like this.
class Player < ApplicationRecord
has_and_belongs_to_many :teams
end
class Team < ApplicationRecord
belongs_to :user
has_many :players
end
The error that results is undefined method `players' for 3:Integer. I've attempted trying singular and plural versions to pull it but same error occurs. Looks like you might be able to do a join table but that wouldn't be ideal or doing a has_many :through association.
Schema
create_table "players", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "full_name"
end
create_table "team_players", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "teams", force: :cascade do |t|
t.bigint "user_id"
t.string "team_name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "r1"
t.integer "r2"
t.integer "r3"
t.integer "r4"
t.integer "r5"
t.integer "r6"
t.integer "r7"
t.integer "r8"
t.integer "r9"
t.integer "r10"
t.index ["user_id"], name: "index_teams_on_user_id"
end
And I set up updating/adding players on a team this way.
<%= f.collection_select(:r1, Player.all, :id, :full_name , {required: true, include_blank: false}, {class: 'uk-input'}) %>
As ConorB said in comments, the r1, r2, etc. bit seems weird. If you want to limit the number of players on a team, I would suggest you do that in code, not in your data structures.
As a note: In your Team model you say:
class Team < ApplicationRecord
belongs_to :user
has_many :players
end
According to the docs:
A has_many association indicates a one-to-many connection with another model.
This is not your case. You have a many-to-many cardinality between Team and Player. So, using has_many without through is a mistake in your case.
What you should have is:
class Player < ApplicationRecord
has_many :team_players
has_many :teams, through: :team_players
end
class Team < ApplicationRecord
has_many :team_players
has_many :players, through: :team_players
end
class TeamPlayer < ApplicationRecord
belongs_to :team
belongs_to :player
end
And then do:
<% #team.players.each do |player| %>
<%= player.full_name %>
<% end %>
You could decide to use has_and_belongs_to_many, in which case you would need a teams_players table, but no TeamPlayer model. (Personally, I never use HABTM, but that's a purely personal preference.) See the guide for more information.
I have articles that are able to be tagged in categories. I am struggling to create a query which will extract the sum of view count (tracked using impressionist gem) of articles within each category.
Schema:
create_table "article_categories", force: :cascade do |t|
t.integer "article_id"
t.integer "category_id"
end
create_table "articles", force: :cascade do |t|
t.string "title"
t.text "description"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "impressions_count", default: 0
end
create_table "categories", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Article model
class Article < ApplicationRecord
is_impressionable :counter_cache => true, :unique => false
belongs_to :user
has_many :impressions, as: :impressionable
has_many :article_categories
has_many :categories, through: :article_categories
end
Category model
class Category < ApplicationRecord
has_many :article_categories
has_many :articles, through: :article_categories
end
ArticleCategory model
class ArticleCategory < ActiveRecord::Base
belongs_to :article
belongs_to :category
end
Here is what i have tried so far and the errors when i test in rails console:
cat_group = Article.joins(:article_categories).group_by(&:category_ids)
// this query results in an array of articles within each category
1) cat_group.sum("impressions_count")
//TypeError: no implicit conversion of Array into String
2) cat_group.select("sum(impressions_count) AS total_count")
//ArgumentError: wrong number of arguments (given 1, expected 0)
3) Article.joins(:article_categories).group(:category_id)
//ActiveRecord::StatementInvalid: PG::GroupingError: ERROR: column "articles.id" must appear in the GROUP BY clause or be used in an aggregate function
If I understand you correctly and view count is impressions_count, here is query that you can use:
Article.joins(:categories).group('categories.name').sum(:impressions_count)
I'm still learning rails so any help you can provide would be super helpful. I've set a count for my likes on my book app. Thus, every time a user likes a book - the number increases by one or decreases if the unlike it. However, if no one has liked a book yet - a 0 appears. I'd like that to be blank so that only when a user has liked it will the number appear. I've listed all my relevant code below. Thank you so much.
Schema.rb
create_table "books", force: :cascade do |t|
t.string "title"
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
t.integer "likes_count", default: 0, null: false
end
create_table "likes", force: :cascade do |t|
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Book.rb
class Book < ApplicationRecord
has_many :likes, :counter_cache => true
has_many :users, through: :likes
belongs_to :user
end
Likes.rb
class Like < ApplicationRecord
belongs_to :book, :counter_cache => true
belongs_to :user
end
Likes Count Migration
class AddLikecountsToBook < ActiveRecord::Migration[5.0]
def change
add_column :books, :likes_count, :integer, :null => false, :default => 0
end
end
With associations in rails you get several interogation methods such as .any? and .none? which can be used to create conditional expressions.
<% if book.likes.any? %>
<%= number_to_human(book.likes.size) %>
<% end %>
# or
<%= number_to_human(book.likes.size) unless book.likes.none? %>
This uses the counter cache as well to avoid n+1 queries.
If you do not want your view to display 0 you could add a if statement in your view.
<% if #votes == 0 %>
be the first to rate this book
<% else %>
<%= #votes %>
<% end %>
Or when returning the variable to the view from the controller
def
if #votes == 0
#votes = ''
end
end
I recently just started learning RoR and I am creating a hobby project.
So some quick background: Each customer is identified by an account number. Each product sale has an account number attributed with it and the products table contains all the specific product data.
My question is - with the format I have now, is this the proper way I should be linking these tables? One of the issues I am having is filtering a group of sales based on product major. So say I have a customer and I only want to view product sales where the major is "commercial", how do I go about filtering this data? See the scope I created - but I am not sure how to use it.
class Product < ActiveRecord::Base
has_many :product_sales, :primary_key => :prodnum, :foreign_key => :prodnum
has_many :customers, through: :sales
scope :commercial_products, -> { where(major: 'Commercial') }
end
class ProductSale < ActiveRecord::Base
belongs_to :customer, :foreign_key => :account
belongs_to :product, :foreign_key => :prodnum, :primary_key => :prodnum
end
class Customer < ActiveRecord::Base
has_many :product_sales, :primary_key => :account, :foreign_key => :account
has_many :products, through: :product_sales
end
and my schema
create_table "customers", force: :cascade do |t|
t.integer "account"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "product_sales", force: :cascade do |t|
t.integer "account"
t.string "month"
t.string "prodnum"
t.integer "sales"
t.integer "qtyshipped"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "products", force: :cascade do |t|
t.string "pcatcode"
t.string "pcatname"
t.string "major"
t.string "prodline"
t.string "brand"
t.string "tier"
t.string "prodnum"
t.string "proddesc"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
... I have a customer and I only want to view product sales where the major is "commercial", how do I go about filtering this data?
This should do it:
#customer.product_sales.where(product: {major: 'Commercial'})
PS. Your models look correct, but this bit
has_many :customers, through: :sales
# Probably you meant: through: product_sales