Rails: destroying multiple user-to-user associations - ruby-on-rails

I have a model called Block that has a blocker_id (a user_id) and a blocked_user_id field (also a user_id). The Block model lets one user block another. When one user blocks another, I want it to destroy the Relationship between them using a before_save method for the Block class. The Relationship table has a follower_id and a followed_id.
This is where things get tricky. I know I could achieve this goal by using multiple return if Relationship.xyz.nil? statements and then using multiple Relationship.find_by(follower_id: , followed_id: ).destroy statements, but this gets to be way over complicated because each blocker and blocked_user could be either the follower and followed id, both, or neither. Is there any easier way to do this?
Here's my models for reference: (also the Block class has a blocked_post field, which I'm having no trouble with)
class Block < ActiveRecord::Base
validates :blocker_id, presence: true
validates :blocked_user_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validates :blocked_post_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validate :blocked_user_or_post
after_validation :validate_block
before_save :destroy_blocked_relationships
belongs_to(
:blocker,
class_name: "User"
)
has_one(
:blocked_user,
class_name: "User"
)
has_one(
:blocked_post,
class_name: "Post"
)
private
def blocked_user_or_post
blocked_user_id.blank? ^ blocked_post_id.blank?
end
def validate_block
if blocked_user_id.present?
!(blocker_id == blocked_user_id)
elsif blocked_post_id.present?
blocked_post = Post.find_by(id: self.blocked_post_id).user_id
!(blocker_id == blocked_post)
else
false
end
end
def destroy_blocked_relationships
#my over-complex code was here
end
end
relationship.rb:
class Relationship < ActiveRecord::Base
validates :follower_id, :followed_id, presence: {message: 'Need an eligible follower and followee id'}
validates :followed_id, uniqueness: { scope: :follower_id}
belongs_to(
:follower,
class_name: "User"
)
belongs_to(
:followed,
class_name: "User"
)
end
If there is any way to do this that doesn't require massive amounts of code, I'd really like to know. Thanks in advance.

I'm not sure of your exact use case, but my thoughts about a system where people can follow each other, it seems that the blocker would always be the person being followed. If this is the case, here's an implementation:
def destroy_blocked_relationships
Relationship.where(follower_id:blocked_user_id, followed_id:blocker_id).destroy_all
true
end
If it makes sense to also block someone from being being followed, you could add this:
Relationship.where(follower_id:blocker_id, followed_id:blocked_user_id).destory_all
Here it is all together, and stopping the save of the Block if there are no relationships:
before_save :destroy_blocked_relationships
def destroy_blocked_relationships
relationships = Relationship.where("(follower_id = ? AND followed_id = ?) OR (followed_id = ? AND follower_id = ? )", blocked_user_id, blocker_id, blocked_user_id, blocker_id)
relationships.destroy_all
relationships.present? # Omit this line if the save should continue regardless
end

here is my understanding:
a Block is a relationship between two users, OR between a User and a Post
when a Block is created between user A and Post X, an implicit block is also created between User A and User B, where User B is Post X's author
Consider making two models, BlockedPost and BlockedUser. Then, make two #make methods. This makes all the related logic easier to reason about.
# class BlockedPost
def make(user, post)
transaction do
create!(user: user, post: post)
BlockedUser.make(user, post.author)
end
end
# class BlockedUser
def make(user, blocked_user)
transaction do
create!(user: user, blocked_user: blocked_user)
Relationship.where(follower: user, following: blocked_user).destroy_all
Relationship.where(follower: blocked_user, following: user).destroy_all
end
end

Related

Rails 5 Model.where(user_id) - Two levels up

Terribly worded, but I'm confusing it.
I have a User model who has_many Clients and has_many statements, through: :clients and then statements which belongs_to clients and belongs to user
In Console I can do all the queries I want. User.statements User.client.first.statements etc - What I'm struggling on is Controller restrictions
For now it's simple - A user should only be able to see Clients and Statements in which they own.
For Clients I did
Client Controller
def index
#clients = Client.where(user_id: current_user.id)
end
Which seems to work perfectly. Client has a field for user_id
I'm kind of stuck on how to emulate this for Statements. Statements do -not- have a user_id field. I'm not quite sure I want them too since in the very-soon-future I want clients to belongs_to_many :users and Statements to not be bound.
Statement Controller
def index
#clients = Client.where(user_id: current_user.id)
#statements = Statement.where(params[:client_id])
end
I'm just genuinely not sure what to put - I know the params[:client_id] doesn't make sense, but what is the proper way to fulfill this? Am I going about it an unsecure way?
Client Model
class Client < ApplicationRecord
has_many :statements
has_many :client_notes, inverse_of: :client
belongs_to :user
validates :name, presence: true
validates :status, presence: true
accepts_nested_attributes_for :client_notes, reject_if: :all_blank, allow_destroy: true
end
Statement Model
class Statement < ApplicationRecord
belongs_to :client
belongs_to :user
validates :name, presence: true
validates :statement_type, presence: true
validates :client_id, presence: true
validates :start_date, presence: true
validates :end_date, presence: true
end
User Model
class User < ApplicationRecord
has_many :clients
has_many :statements, through: :clients
end
Based on the reply provided below I am using
def index
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
#statements = #clients.statements
else
return 'error'
end
end
Unsure if this logic is proper
Use includes to avoid [N+1] queries.
And regarding "A user should only be able to see Clients and Statements in which they own".
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
# do more
else
# Type your error message
end
Additionally, you might need to use strong params and scope.
The best way to do it is using includes:
#clients = Client.where(user_id: current_user.id)
#statements = Statement.includes(clients: :users}).where('users.id = ?', current_user.id)
You can take a look in here: https://apidock.com/rails/ActiveRecord/QueryMethods/includes
In this case, thanks to the reminder that current_user is a helper from Devise, and the relational structure I showed, it was actually just as simple as
def index
#statements = current_user.statements
end
resolved my issue.
Due to the [N+1] Queries issue that #BigB has brought to my attention, while this method works, I wouldn't suggest it for a sizable transaction.

Make sure has_many :through association is unique on creation

If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).

rails 4 complex model validation

I have a rails app. Assigner(current_user) must assign a task to executor(other user). I'm using getter setter method for jquery autocomplete and with the validation so I can make sure if there is an existing user who the task is assigned to. I'm using the rescue set to nil so either if the field empty or the user is non-existing can be validated with presence of. I'd like to change this, so users either could leave the field empty or choosing from existing users. As I'm validating the executor object I'm not sure how I can do that.
task.rb
class Task < ActiveRecord::Base
belongs_to :assigner, class_name: "User"
belongs_to :executor, class_name: "User"
validates :assigner, presence: true
validates :executor, presence: { message: "must be valid"}
def task_name_company
[executor.try(:profile).try(:first_name), executor.try(:profile).try(:last_name), executor.try(:profile).try(:company)].join(' ')
end
def task_name_company=(name)
self.executor = User.joins(:profile).where("CONCAT_WS(' ', first_name, last_name, company) LIKE ?", "%#{name}%").first if name.present?
rescue ArgumentError
self.executor = nil
end

Create two models at same time with validation

I'm having a potluck where my friends are coming over and will be bringing one or more food items. I have a friend model and each friend has_many food_items. However I don't want any two friends to bring the same food_item so food_item has to have a validations of being unique. Also I don't want a friend to come (be created) unless they bring a food_item.
I figure the best place to conduct all of this will be in the friend model. Which looks like this:
has_many :food_items
before_create :make_food_item
def make_food_item
params = { "food_item" => food_item }
self.food_items.create(params)
end
And the only config I have in the food_item model is:
belongs_to :friend
validates_uniqueness_of :food_item
I forsee many problems with this but rails is telling me the following error: You cannot call create unless the parent is saved
So how do I create two models at the same time with validations being checked so that if the food_item isn't unique the error will report properly to the form view?
How about to use nested_attributes_for?
class Friend < ActiveRecord::Base
has_many :food_items
validates :food_items, :presence => true
accepts_nested_attributes_for :food_items, allow_destroy: true
end
You're getting the error because the Friend model hasn't been created yet since you're inside the before_create callback. Since the Friend model hasn't been created, you can't create the associated FoodItem model. So that's why you're getting the error.
Here are two suggestions of what you can do to achieve what you want:
1) Use a after_create call back (I wouldn't suggest this since you can't pass params to callbacks)
Instead of the before_create you can use the after_create callback instead. Here's an example of what you could do:
class Friend
after_create :make_food_item
def make_food_item
food_params = # callbacks can't really take parameters so you shouldn't really do this
food = FoodItem.create food_params
if food.valid?
food_items << food
else
destroy
end
end
end
2) Handle the logic creation in the controller's create route (probably best option)
In your controller's route do the same check for your food item, and if it's valid (meaning it passed the uniqueness test), then create the Friend model and associate the two. Here is what you might do:
def create
friend_params = params['friend']
food_params = params['food']
food = FoodItem.create food_params
if food.valid?
Friend.create(friend_params).food_items << food
end
end
Hope that helps.
As mentioned, you'll be be best using accepts_nested_attributes_for:
accepts_nested_attributes_for :food_items, allow_destroy: true, reject_if: reject_if: proc { |attributes| attributes['foot_item'].blank? }
This will create a friend, and not pass the foot_item unless one is defined. If you don't want a friend to be created, you should do something like this:
#app/models/food_item.rb
Class FootItem < ActiveRecord::Base
validates :[[attribute]], presence: { message: "Your Friend Needs To Bring Food Items!" }
end
On exception, this will not create the friend, and will show the error message instead

In Rails, how can I create group of users as another association, such as "members"?

I am trying to create a special relationship between two existing models, User and Dwelling. A Dwelling has only one owner (Dwelling belongs_to :user, User has_one :dwelling) at the time of creation. But other Users can be added to this Dwelling as Roomies (there is no model created for this now, Roomie is a conceptual relationship).
I don't think I need a separate model but rather a special relationship with the existing models, but I could be wrong. I think the reference needs to be made with user_id from the Users table. I'm not really sure where to start this. Thank you for any and all help!
For example:
Dwelling1
user_id: 1
roomies: [1, 2, 3, 4]
Where 1, 2, 3, 4 are user_ids.
Updated Models
Dwelling Model
# dwelling.rb
class Dwelling < ActiveRecord::Base
attr_accessible :street_address, :city, :state, :zip, :nickname
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
validates :street_address, presence: true
validates :city, presence: true
validates :state, presence: true
validates :zip, presence: true
end
User Model
# user.rb
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name, :password, :password_confirmation, :zip
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
belongs_to :dwelling
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id"
validates :first_name, presence: true, length: { maximum: 50 }
...
Updated Dwelling Create Action
#dwellings_controller.rb
...
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
flash[:success] = "Woohoo! Your dwelling has been created. Welcome home!"
redirect_to current_user
else
render 'new'
end
end
end
...
My answer assumes you only want a user to be a roomie at one dwelling. If you want a user to be a roomie at more than one dwelling, I think #ari's answer is good, although I might opt for has_and_belongs_to_many instead of has_many :through.
Now for my answer:
I would set it up so that a dwelling belongs_to an owner and has_many roomies (including possibly the owner, but not necessarily).
You can use the User model both for owners and roomies. You don't need any additional tables or models, you just need to setup the proper relationships by using the :class_name and :foreign_key options.
In your Dwelling model:
# dwelling.rb
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
In your User model:
# user.rb
belongs_to :dwelling # This is where the user lives
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id" # This is the dwellings the user owns
In your dwellings table you need an owner_id column to store the user_id of the owner
In your users table you need a dwelling_id to store the dwelling_id of the dwelling where the user lives.
To answer your question in the comments regarding the controller:
If you want to setup current_user as the owner of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling])
....
If you want to setup the current_user as the owner AND a roomie of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling]
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
# flash and redirect go here
else
# It's not clear why this wouldn't save, but you'll to determine
# What to do in such a case.
end
else
...
end
The trickiest part of above is handling the case that the dwelling is valid and saves, but for some unrelated reason the current_user can't be saved. Depending on your application, you may want the dwelling to save anyway, even if you can't assign the current_user as a roomie. Or, you might want the dwelling not to be saved --- if so, you'd need to use a model transaction, which is bit beyond the scope of this question.
Your controller code didn't work because saving the Dwelling doesn't actually update the current_user record to store the dwelling_id. Your code would be equivalent to the following:
#dwelling = Dwelling.new(params[:dwelling])
current_user.dwelling = #dwelling
if #dwelling.save
...
Note that current_user is never saved, so the current_user.dwelling = #dwelling line is useless.
This might seem counter-intuitive, but the bottom line is that build_dwelling isn't actually setting up things in memory as you might expect. You'd achieve more intuitive results if you saved the model you're building from rather than the model you're building:
#dwelling = current_user.build_dwelling(params[:dwelling])
if current_user.save # This will save the dwelling (if it is valid)
However, this (by default) won't save the dwelling if it has validation errors unless you turn :autosave on for the association, which is also a bit beyond the scope of this question. I really wouldn't recommend this approach.
Update:
Here is a more detailed code snippet:**
# dwellings_controller.rb
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
# The current user is now the owner, but we also want to try to assign
# his as a roomie:
current_user.dwelling = #dwelling
if current_user.save
flash[:notice] = "You have successfully created a dwelling"
else
# For some reason, current_user couldn't be assigned as a roomie at the
# dwelling. This could be for several reasons such as validations on the
# user model that prevent the current_user from being saved.
flash[:notice] = "You have successfully created a dwelling, but we could not assign you to it as a roomie"
end
redirect_to current_user
else
# Dwelling could not be saved, so re-display the creation form:
render :new
end
end
When a dwelling saves successfully, the current user will be the owner (owner_id in the database). However, if the current_user doesn't save, you'll need to decide how your application should respond to that. In the example above, I allow the dwelling to be saved (i.e. I don't rollback its creation), but I inform the user that he couldn't be assigned as a roomie. When this happens, it's most likely other code in your application causing the problem. You could examine the errors of current_user to see why. Or, you could use current_user.save! instead of current_user.save temporarily to troubleshoot.
Another way to do all of this is with an after_create callback in the Dwelling model. In many ways that would be a cleaner and simpler way to do it. However, catching the case when the current_user can't be saved could be even uglier than the method above, depending on how you want to handle it.
I believe the bottom line is that the current_user.save code is causing some problems. You'll need to diagnose why, and then determine what your application should do in that case. There are several ways to handle this, including at least the following
Put everything in a transaction block, and use current_use.save! instead of current_user.save so that an exception is raised and neither the user or dwelling is saved.
Save the dwelling, but inform the user that he isn't a roomie (As above)
Instead of saving the current_user, use update_column (which avoids callbacks, validations, etc.).
I believe the current problems you're experiencing are essentially unrelated to the original question. If you need further assistance, it might be best to break it off as a separate question.
You could do this by storing Roomie ids as a column in Dwelling
Make a migration:
class AddRoomiesToDwelling < ActiveRecord::Migration
def self.up
add_column :dwelling, :roomies, :text
end
def self.down
remove_column :dwelling, :roomies
end
end
In your Dwelling model:
class Dwelling < ActiveRecord::Base
serialize :roomies
end
You can then set the roomie ids with:
roomie_ids = [1, 2, 3, 4]
#dwelling.roomies = {:ids => roomie_ids}
#dwelling.save!
Taken from the Saving arrays, hashes, and other non-mappable objects in text columns section of this
You have two possible options.
Depending on your plan, it might be clearer for the dwelling to have_one owner instead of the owner having one dwelling. Then the dwelling would also be able to have users. You can add a column to User called dwelling_id and then you could do dwelling has_many users.
Another option would be to use the "has_many through" association. This means you would need to create a new model that would keep track of this association, say "Relationship.rb", which would belong to both User and Dwelling (and have columns for both for them). Then you would be able to write code like this:
//in Dwelling.rb
has_many :roomies, through: :relationships, source: :user
//in User.rb
has_many :dwellings, through: :relationships
This would let users also join more than one dwelling.

Resources