Rails - Model Associations - User and Product - Custom methods - ruby-on-rails

I'm creating an app with two main models : User and Product. User can have many products as an owner, and many products as a borrower. Product have only one owner, but can have many seekers, including a borrower. I associated them directly for the owning property, but for the borrowing property, I created a Transaction model. The three look like this :
app/models/transaction.rb
class Transaction
# has a seeker_id:integer, a product_id:integer and a current:boolean
before_save :check_current
# Associations
belongs_to :seeker, class_name: "User", foreign_key: "seeker_id"
belongs_to :product
# Methods
def check_current
if !self.borrowing_date.nil? && self.return_date.nil?
self.current = true
end
end
end
A product has many transactions, but it can be borrowed by only one seeker at the time. When the product is borrowed, the transaction has a borrowing_date that is not nil, and a return_date that is nil. Then the check_current method toggles the current boolean of this transaction from false to true. The seeker of that current transaction is specified as a borrower.
app/models/user.rb
class User
.
.
.
has_many :owned_products, class_name: "Product", foreign_key: "owner_id", dependent: :destroy
has_many :transactions, foreign_key: "seeker_id", dependend: :destroy
has_many :requested_products, through: :transactions, source: :product
has_many :active_transactions, -> { where current: true },
class_name: 'Transaction',
foreign_key: "seeker_id",
dependent: :destroy
has_many :borrowed_products, through: :active_transactions,
source: :product
def requesting?(product)
self.transactions.find_by(product_id: product.id)
end
def request!(product)
self.transactions.create!(product_id: product.id)
end
def borrowing?(product)
self.transactions.find_by(product_id: product.id, current: true)
end
def borrowed_products
self.transactions.where(current: :true).product
end
end
app/models/products.rb
class Product
.
.
.
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
has_many :transactions, dependent: :destroy
has_many :seekers, through: :transactions,
source: :seeker
def borrowed?
self.transactions.find_by(current: true)
end
def borrower
self.transactions.find_by(current: true).seeker
end
end
When I testing some of my code, five of the tests fail, the same type, and I don't understand why :
describe User do
before { #user = User.new(name: "Utilisateur de test",
email: "test#utilisateur.com",
password: "motdepasse",
password_confirmation: "motdepasse") }
subject { #user }
describe "requested product associations" do
let(:lender) { FactoryGirl.create(:user) }
let(:product) { FactoryGirl.create(:product, owner: lender) }
before do
#user.save
#user.request!(product)
end
it { should be_requesting(product) }
its(:requested_products) { should include(product) } # FAIL
describe "when product is borrowed" do
before do
transaction = Transaction.find_by(product: product)
transaction.update_attributes(borrowing_date: 1.day.ago)
transaction.save
end
it { should be_borrowing(product) }
its(:requested_products) { should_not include(product) } # FAIL
its(:borrowed_products) { should include(product) } # FAIL
describe "then returned" do
before do
transaction = Transaction.find_by(product: product)
transaction.update_attributes(return_date: 1.hour.ago)
end
it { should_not be_borrowing(product) }
its(:requested_products) { should_not include(product) } # FAIL
its(:borrowed_products) { should_not include(product) } # FAIL
end
end
end
end
Here are the error messages :
1) User requested product associations requested_products
Failure/Error: its(:requested_products) { should include(product) }
ActiveRecord::StatementInvalid:
SQLite3::SQLException: ambiguous column name: created_at: SELECT 1 AS one FROM "products" INNER JOIN "transactions" ON "products"."id" = "transactions"."product_id" WHERE "transactions"."seeker_id" = ? AND "products"."id" = 1 ORDER BY created_at DESC LIMIT 1
# ./spec/models/user_spec.rb:174:in `block (3 levels) in <top (required)>'
2) User requested product associations when product has been borrowed borrowed_products
Failure/Error: its(:borrowed_products) { should include(product) }
ActiveRecord::StatementInvalid:
SQLite3::SQLException: ambiguous column name: created_at: SELECT 1 AS one FROM "products" INNER JOIN "transactions" ON "products"."id" = "transactions"."product_id" WHERE "transactions"."seeker_id" = ? AND "transactions"."current" = 't' AND "products"."id" = 1 ORDER BY created_at DESC LIMIT 1
# ./spec/models/user_spec.rb:185:in `block (4 levels) in <top (required)>'
3) User requested product associations when product has been borrowed requested_products
Failure/Error: its(:requested_products) { should_not include(product) }
ActiveRecord::StatementInvalid:
SQLite3::SQLException: ambiguous column name: created_at: SELECT 1 AS one FROM "products" INNER JOIN "transactions" ON "products"."id" = "transactions"."product_id" WHERE "transactions"."seeker_id" = ? AND "products"."id" = 1 ORDER BY created_at DESC LIMIT 1
# ./spec/models/user_spec.rb:184:in `block (4 levels) in <top (required)>'
4) User requested product associations when product has been borrowed then returned requested_products
Failure/Error: its(:requested_products) { should_not include(product) }
ActiveRecord::StatementInvalid:
SQLite3::SQLException: ambiguous column name: created_at: SELECT 1 AS one FROM "products" INNER JOIN "transactions" ON "products"."id" = "transactions"."product_id" WHERE "transactions"."seeker_id" = ? AND "products"."id" = 1 ORDER BY created_at DESC LIMIT 1
# ./spec/models/user_spec.rb:195:in `block (5 levels) in <top (required)>'
5) User requested product associations when product has been borrowed then returned borrowed_products
Failure/Error: its(:borrowed_products) { should_not include(product) }
ActiveRecord::StatementInvalid:
SQLite3::SQLException: ambiguous column name: created_at: SELECT 1 AS one FROM "products" INNER JOIN "transactions" ON "products"."id" = "transactions"."product_id" WHERE "transactions"."seeker_id" = ? AND "transactions"."current" = 't' AND "products"."id" = 1 ORDER BY created_at DESC LIMIT 1
# ./spec/models/user_spec.rb:196:in `block (5 levels) in <top (required)>'
But when I run some tests manually in the rails console, the user.borrowed_products and user.requested_products work just fine. Weird ???

For the first failing test
def borrowed_products
self.transactions.where(current: :true).product
end
The above method checks for current: true. I don't see you setting the attribute in your transaction setup.
before do
transaction = Transaction.find_by(product: product)
transaction.update_attributes(borrowing_date: 1.day.ago) #Why are you setting borrowing date. How is borrowing date and current related?
transaction.save
end
For the Second test.
requested_products association is established through transactions. You are not setting up a transaction. Is it done in your factory?

OK I found ! Yippee-ki-yay !
The error messages were telling me the created_at column was ambiguous. But why ? Because there are as much created_at column as there are associated models ! So it had something to do with it. But where the created_at appeared in my code ?
I checked my app/models/transaction.rb, my app/models/user.rb and my app/models/product.rb, and in this last model, I found the line :
default_scope -> { order('created_at DESC') }
That I changed to that, just to try :
default_scope -> { order('name DESC') }
And everything went just fine !
But now, if I want to scope it by created_at, I don't know how to do it :-p

Related

postgres - distinct order_by a jsonb key throws a rails PG::InvalidColumnReference

My user model has the following definition for the follower association:
has_many :passive_follow_actions, -> { where("actions.activity_verb = 'follow'").uniq }, class_name: 'Action', foreign_key: :activity_object_id
has_many :followers, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
#
User table has a jsonb field called follow_stats which I use to order the follower results.
On its own, a distinct operation works
has_many :followers, -> { distinct }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User'
#
Also on its own, an order operation works
has_many :followers, -> { order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
However, put together,
has_many :followers, -> { distinct.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
it throws the following exception:
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
#
I tried adding the jsonb query onto the select clause to prevent the error, but that results in an empty response:
has_many :followers, -> { select("follow_stats->'followers_count'").uniq.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT follow_stats->'followers_count' FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
Response:
[#<User id: nil, ?column?: 42>, #<User id: nil, ?column?: 4>, #<User id: nil, ?column?: 2>, #<User id: nil, ?column?: 1>, #<User id: nil, ?column?: 0>]
#
When I add a * in the select clause, it returns the same exception PG::InvalidColumnReference
has_many :followers, -> { select("*, follow_stats->'followers_count'").uniq.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
#SELECT DISTINCT *, follow_stats->'followers_count' FROM \"users\" INNER JOIN \"actions\" ON \"users\".\"id\" = \"actions\".\"activity_actor_id\" WHERE \"actions\".\"activity_object_id\" = 1 AND (actions.activity_verb = 'follow') AND \"actions\".\"activity_actor_type\" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
How select distinct and order by a jsonb key at the same time?

Rails belongs_to STI table with scope

I've a STI table "parties" as follows:
# app/models/party.rb
class Party < ApplicationRecord
has_many :party_contacts
scope :vendors, -> { where(type: 'Party::Vendor') }
scope :customers, -> { where(type: 'Party::Customer') }
end
# app/models/party/vendor.rb
class Party::Vendor < Party
end
# app/models/party/customer.rb
class Party::Customer < Party
end
And "party_contacts" table as follow:
class PartyContact < ApplicationRecord
belongs_to :party
scope :of_vendors, -> {# fetch all contacts belongs to all vendors logic }
scope :of_customers, -> {# fetch all contacts belongs to all customers logic }
end
I want to make query on "party_contacts" to get list of all vendors/customers contacts. How can I write the scope for "party_contacts" (or should it be in parent model)?
I'm trying following scopes:
scope :of_vendors, -> { joins(:party).includes(:party).where( party: { type: "Party::Vendor" } ) }
scope :of_customers, -> { joins(:party).includes(:party).where( party: { type: "Party::Customer" } ) }
But get error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "party"
LINE 1: ...parties"."id" = "party_contacts"."party_id" WHERE "party"."t...
Thank you #Swards, I figured it out just a while before. The scope should be as follows:
scope :of_vendors, -> { joins(:party).includes(:parties).where( parties: { type: "Party::Vendor" } ) }
scope :of_customers, -> { joins(:party).includes(:parties).where( parties: { type: "Party::Customer" } ) }
It constructs right query:
2.4.0 :042 > PartyContact.of_vendors
PartyContact Load (8.8ms) SELECT "party_contacts".* FROM "party_contacts" INNER JOIN "parties" ON "parties"."id" = "party_contacts"."party_id" WHERE "parties"."type" = $1 [["type", "Party::Vendor"]]
=> #<ActiveRecord::Relation []>
2.4.0 :043 > PartyContact.of_customers
PartyContact Load (0.5ms) SELECT "party_contacts".* FROM "party_contacts" INNER JOIN "parties" ON "parties"."id" = "party_contacts"."party_id" WHERE "parties"."type" = $1 [["type", "Party::Customer"]]
=> #<ActiveRecord::Relation []>
Here party is singular in joins(:party) as it is belongs_to relation. And parties is plural in includes(:parties) and where(parties:{...}) as it is name of table.

Rails order by a field in parent belongs_to association

I have three models in my Rails app, User, Number, and Message:
class User < ActiveRecord::Base
has_many :numbers
has_many :messages, through: :numbers
end
class Number < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :number
end
Number migration file has:
t.string :digits, index: true # Example: '14051234567' (no + sign)
In my controller:
sort_mode = # asc or desc
#messages = current_user.messages.order(???)
The thing is that I want to sort those messages by their numbers' digits.
How to do that dynamically (depending on sort_mode)?
EDIT:
sort_mode = 'asc'
#messages = current_user.messages.includes(:number)
order = { number: { digits: sort_mode } }
#messages = #messages.order(order)
^ Doesn't work. Second argument must be a direction.
Also, order('number.digits': sort_mode) throws:
SQLite3::SQLException: no such column: messages.number.digits: SELECT "messages".* FROM "messages" INNER JOIN "numbers" ON "messages"."number_id" = "numbers"."id" WHERE "numbers"."user_id" = ? ORDER BY "messages"."number.digits" ASC LIMIT 10 OFFSET 0
You'll need to use includes. Try:
#messages = current_user.messages.includes(:number).order('numbers.digits ASC')

SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets"

I really don't think the title of this explains well of what I'm trying to do but I'm not even sure how to ask.
So I have ticket has_many tasks and task belongs_to account. I've this as a scope to return the ticket listing where an tickets task belongs to an account:
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
but it's returning multiple of the same ticket because a ticket has multiple tasks that the account belongs to.
How can I get it to only return each ticket once rather for each task in that ticket that an account belongs to?
Thanks!
Update
I'd actually like to combine to scopes to list all that apply to the two lambdas:
scope :for_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) } || lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
Is this possible? As well as the first issue.
Update 2
I've figured out how to get both of the queries to be combined but I'm still getting multiple of the same ticket in the returned query.
scope :for_group_with_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) }
scope :for_task_with_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
scope :for_account, lambda { |account| for_group_with_account(account) & for_task_with_account(account).select('DISTINCT id') }
I'm using DISTICNT but I still get
SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets" INNER JOIN "groups" ON "groups"."id" = "tickets"."group_id" INNER JOIN "assignments" ON "groups"."id" = "assignments"."group_id" INNER JOIN "accounts" ON "accounts"."id" = "assignments"."account_id" INNER JOIN "tasks" ON "tasks"."ticket_id" = "tickets"."id" INNER JOIN "accounts" "accounts_tasks" ON "accounts_tasks"."id" = "tasks"."account_id" WHERE ("tickets"."archived" IS NULL) AND (accounts.id = 20) LIMIT 20 OFFSET 0
Thanks again!
I think you should be able to use "distinct" in this scenario.
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id).select('distinct accounts.id') }

Weird relationship behavior on Rails 3 and update_attribute

I'm having a hard time trying to find out why a test is failing:
describe User, "Instance Methods" do
describe "leave_group!" do
it "should set group_id to nil" do
#user = Factory(:user)
#group_2 = Factory(:group, :owner => #user)
#user.leave_group!
#user.reload.group_id.should be_nil # THIS LINE IS FAILING!!!
end
it "should assign another owner to group, if owner was user" do
#user = Factory(:user)
#group = Factory(:group, :owner => #user)
1.upto(4) { #group.add_user Factory(:user) }
#user.leave_group!
#group.reload.owner.should_not eql(#user)
end
end
end
These are the models I'm using:
class User < ActiveRecord::Base
# Associations
has_one :own_group, :class_name => "Group", :foreign_key => "owner_id"
belongs_to :group
def leave_group!
current_group_id, current_group_owner = self.group.id, self.group.owner
self.group_id = nil
save!
Group.find(current_group_id).randomize_owner! if current_group_owner == self
end
end
class Group < ActiveRecord::Base
# Associations
has_many :users
belongs_to :owner, class_name: "User"
def randomize_owner!
current_users = self.users
return false unless current_users.length > 1
begin
new_user = current_users.sort_by { rand }.first
end while self.owner == new_user
self.owner_id = new_user.id
save!
end
end
Am I doing something wrong here? Could I improve it? And more importantly, why is that single test failing?
Here's the log output for runing that single test:
SQL (0.2ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
AREL (0.3ms) INSERT INTO "users" ("name", "uid", "provider", "email", "image_url", "group_id", "created_at", "updated_at", "timezone", "public_readings", "is_visible_on_leaderboards", "phone_number") VALUES ('John Doe', '314159265', 'facebook', 'john#does.com', NULL, NULL, '2011-07-18 02:02:08.455229', '2011-07-18 02:02:08.455229', NULL, 't', 't', NULL)
Group Load (0.1ms) SELECT "groups".* FROM "groups" WHERE "groups"."key" = 'SNYEMJ' LIMIT 1
AREL (0.1ms) INSERT INTO "groups" ("name", "key", "score", "owner_id", "created_at", "updated_at") VALUES ('John''s Group', 'SNYEMJ', 0, 1, '2011-07-18 02:02:08.522442', '2011-07-18 02:02:08.522442')
AREL (0.0ms) UPDATE "users" SET "group_id" = 1, "updated_at" = '2011-07-18 02:02:08.524342' WHERE "users"."id" = 1
Group Load (0.1ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 1 LIMIT 1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE ("users".group_id = 1)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
The last 3 lines are all selects, notice rails doesn't even try to remove the group_id from the user. (There are 2 inserts, 1 for the test user and 1 for the test group and 1 update which assigns group_id to the test user).
Try adding a #user.reload call before #user.leave_group in the test.
Even though the user record is updated with it's group in the DB when you create #group_2 from the factory, I suspect the #user object is not. Then you call leave_group! with a #user with a group ID of nil, so the save won't do anything because the object is unchanged. Then in the next line of your test you reload the #user, which now has the group_id from the DB assigned earlier.
try to chance the model class as following:
class User < ActiveRecord::Base
# Associations
has_one :own_group, :class_name => "Group", :foreign_key => "owner_id"
belongs_to :group
def leave_group!
group_randomize_owner
self.group.clear
end
private
def group_randomize_owner
if group.owner == self
group.randomize_owner!
end
end
end

Resources