I have built a user and friend relationship model but the problem is that with those associations I can friend myself. I have successfully suppressed it in my views and controller, but logically it should be suppressed in the model because I could still create the friendship from the console which I want to avoid.
User model
has_many :user_friendships
has_many :friends, through: :user_friendships,
conditions: { user_friendships: { state: 'accepted' } }
User_friendship model
belongs_to :user
belongs_to :friend, class_name: 'User', foreign_key: 'friend_id'
Everything else is working perfectly like adding, blocking, deleting, requesting a friend the only problem with my model is that I can also friend myself which I want to avoid.
Add a validation to UserFriendship:
validate :cannot_friend_self
def cannot_friend_self
errors.add(:friend_id, "cannot friend self") unless user_id != friend_id
end
This issue is a little problematic because we want to remain RESTful, separate the different tasks (MVC,) and take into account of weird race conditions (Thread Safety.)
Try using validations#exclusions (http://guides.rubyonrails.org/active_record_validations_callbacks.html#exclusion)
class ApplicationController < ActionController::Base
...
before_filter do |c|
User.current_user = User.find(c.session[:user]) unless c.session[:user].nil?
end
...
end
class User < ActiveRecord::Base
...
cattr_accessor :current_user
...
end
class Friends < ActiveRecord::Base
...
validates :friend_id, :exclusion => { :in => %w(User.current_user.id),
:message => "I don't think you really want to friend yourself" }
...
end
If you want to be safe, please refer to (http://nhw.pl/wp/2011/11/30/passing-current-user-id-to-rails-models)
Disclaimer:
I wrote this possible solution without testing it (aka pulled it out of the thin air with little reference)
I have not thread with Ruby on Rails.
You probably want to throw in a validation
Such as
validate :cannot_friend_self
def cannot_friend_self
current_user.id != friend.id
end
This code may not be exactly what you want, but should point you in the right direction.
Full guide here http://guides.rubyonrails.org/active_record_validations_callbacks.html#custom-methods
Related
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 7 years ago.
Improve this question
I am creating a simple chatting app on rails 4. The controllers, models and views are created but the functionality is still incomplete. I have 2 tables in my database, conversations and messages. The conversation table holds two fields, sender id and receiver id. And the messages table holds 3 fields, body, user id and read(defaults to 0 meaning not read).
Models:
class Conversation < ActiveRecord::Base
belongs_to :sender, :foreign_key => :sender_id, :class_name => "User"
belongs_to :reciever, :foreign_key => :reciever_id, :class_name => "User"
has_many :messages, :dependent => :destroy
validates_uniqueness_of :sender_id, :scope => :reciever_id
scope :involving, lambda { |user_id|
where("sender_id = ? OR reciever_id = ?", user_id, user_id)
}
scope :between, lambda { |sender_id, reciever_id|
where("(sender_id = ? AND reciever_id = ?) OR (sender_id = ? AND reciever_id = ?)", sender_id, reciever_id, reciever_id, sender_id)
}
def other_interlocutor(user_id)
if sender.id == user_id
return reciever.id
else
return sender.id
end
end
end
class Message < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
validates_presence_of :conversation_id, :user_id, :body
end
What I am trying to do is to create a real-time functionality of receiving unread messages count whenever someone receives a new message. I am using private pub to create the chat between users.
I have a user model which contains this function:
def unread_messages_count
unread_messages = 0
# puts "Putting self conversations ! #{self.conversations.first}"
conversations = Conversation.involving(self.id)
conversations.each do |conversation|
unread_messages += conversation.messages.where(:read => 0, :user_id => conversation.other_interlocutor(self.id)).count
end
return unread_messages = unread_messages == 0 ? nil : unread_messages
end
I have one page where all user's conversations are listed and one a conversation is clicked all the messages related to that conversation are listed too. On the same page I have subscribed every conversation_messages_path to create separate channels for every conversation. Whenever a message is sent a create.js.erb file is rendered where I publish to those subscribed channels:
<% publish_to conversation_messages_path(#conversation.id) do %>
$("#conversations_link").text("<%= current_user.unread_messages_count %> Conversations");
$("#messages").append("<%= escape_javascript render(:partial => 'message', :locals => { :message => #message })%>");
<% end %>
The $("#conversation_link") is where I want to show the unread messages count.
Currently, the unread messages count is returning back the wrong count and the navbar is only updated when the conversation.sender_id messages the receiver.
My unread message counter is not returning the correct number of unread messages. I don't know how to fix it. What's wrong in my code?
Thanks.
I would argue that your domain modeling is really off.
The whole idea of a conversation is that the parties involved take turns being the the sender and recipient. What your have modeled is a monologue.
A monologue is a speech delivered by one person, or a long one-sided
conversation that makes you want to pull your hair out from boredom.
The Greek root word monologos translates to “speaking alone,” and
that's a monologue: one person doing all the talking.
The domain model you end up with here should look something like this:
Its the message that is actually linked to two users (or more): the sender and the recipient. For the sake of simplicity i'm sticking to 1:1 messaging here (vs group chats where a message may belong to many recipients).
class Message
belongs_to :recipient, class_name: 'User'
belongs_to :sender, class_name: 'User'
end
class User
has_many :sent_messages,
class_name: 'Message',
foreign_key: 'sender_id'
has_many :messages, foreign_key: 'recipient_id'
end
Note that we need to tells Rails the class and the foreign key when it cannot be derived from the association name.
Instead of having a boolean read field you might want to consider using a enum to denote the status of the message.
Enums are basically an integer column which maps to a list of symbols.
class Message
enum :status, [:unread, :read]
belongs_to :recipient, class_name: 'User'
belongs_to :sender, class_name: 'User'
belongs_to :conversation
end
Enums give you scopes like:
Message.unread
Message.read
And conditionals such as:
message.unread?
message.read?
And it makes it very straight forward if you want to add more states such as :archieved or :trashed.
Armed with this you don't need your unread_messages_count monstrosity. Which will eat tons of memory since you are pulling records out of the database just to count the associated records.
current_user.messages.unread.size
Also we should define the relation between User and Conversation correctly:
class Conversation
has_many :messages
has_and_belongs_to_many :users
end
class Users
# ..
has_and_belongs_to_many :conversations
end
This has_and_belongs_to_many relation would store users and converstations in the users_conversations join table. You can use the following generator to create the join table migration:
rails generate migration users_conversations
Added:
Using session[:user_id] in your views is a really bad code smell. You are tightly coupling your authentication logic all over your application.
Instead create a helper:
class SessionsHelper
def current_user
#current_user ||= User.find(session[:user_id])
end
def user_signed_in?
!current_user.nil?
end
end
Other parts of the application should not know that you store the current user in session[:user_id] just that there is a current_user.
I'm associating users with given firms through a join table because I need to be able to have a bunch of users with every firm and vice versa.
class User
has_many :firm_connections, dependent: :destroy
has_many :firms, through: :firm_connections
end
class FirmConnection
belongs_to :user
belongs_to :firm
end
class Firm
has_many :firm_connections
has_many :users, through: :firm_connections
end
My question is, when a user hits the index page for firms, how do I scope it to only show what those users are associated with?
class FirmPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where #only the firms associated with that user
end
end
end
Do I need to create a scope at the firm level that accepts a #user? Or can I do this all directly inline? I could hack something together, but haven't wrapped my head around pundit yet, so any direction would be greatly appreciated!
like this:
def self.associated_with(user)
all.select { |m| m.users.include?(user) }
end
This should work for you
class Firm
def index
#firms = policy_scope(Firm)
end
end
class FirmPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
user.firms #only the firms associated with that user
end
end
end
end
The policy doesn't always have to call it the way that you think, it just has to return something (for scopes, almost always an ActiveRecord::Relation, for regular, true or false). You could do
scope.includes(:firm_connections).where(firm_connections: { user_id: user.id })
but that's not as readable (IMO).
I'm rendering a model and it's children Books in JSON like so:
{"id":2,"complete":false,"private":false, "books" [{ "id":2,"name":"Some Book"},.....
I then come to update this model by passing the same JSON back to my controller and I get the following error:
ActiveRecord::AssociationTypeMismatch (Book (#2245089560) expected, got ActionController::Parameters(#2153445460))
In my controller I'm using the following to update:
#project.update_attributes!(project_params)
private
def project_params
params.permit(:id, { books: [:id] } )
end
No matter which attributes I whitelist in permit I can't seem to save the child model.
Am I missing something obvious?
Update - another example:
Controller:
def create
#model = Model.new(model_params)
end
def model_params
params.fetch(:model, {}).permit(:child_model => [:name, :other])
end
Request:
post 'api.address/model', :model => { :child_model => { :name => "some name" } }
Model:
accepts_nested_attributes_for :child_model
Error:
expected ChildModel, got ActionController::Parameters
Tried this method to no avail: http://www.rubyexperiments.com/using-strong-parameters-with-nested-forms/
Are you using accepts_nested_attributes_for :books on your project model? If so, instead of "books", the key should be "books_attributes".
def project_params
params.permit(:id, :complete, :false, :private, books_attributes: [:id, :name])
end
I'm using Angular.js & Rails & Rails serializer, and this worked for me:
Model:
has_many :features
accepts_nested_attributes_for :features
ModelSerializer:
has_many :features, root: :features_attributes
Controller:
params.permit features_attributes: [:id, :enabled]
AngularJS:
ng-repeat="feature in model.features_attributes track by feature.id
My solution to this using ember.js was setting the books_attributes mannualy.
In controller:
def project_params
params[:project][:books_attributes] = params[:project][:books_or_whatever_name_relationships_have] if params[:project][:books_or_whatever_name_relationships_have]
params.require(:project).permit(:attr1, :attr2,...., books_attributes: [:book_attr1, :book_attr2, ....])
end
So rails checks and filters the nested attributes as it expected them to come
This worked for me. My parent model was an Artist and the child model was a Url.
class ArtistsController < ApplicationController
def update
artist = Artist.find(params[:id].to_i)
artist.update_attributes(artist_params)
render json: artist
end
private
def artist_params
remap_urls(params.permit(:name, :description, urls: [:id, :url, :title, :_destroy]))
end
def remap_urls(hash)
urls = hash[:urls]
return hash unless urls
hash.reject{|k,v| k == 'urls' }.merge(:urls_attributes => urls)
end
end
class Artist < ActiveRecord::Base
has_many :urls, dependent: :destroy
accepts_nested_attributes_for :urls, allow_destroy: true
end
class Url < ActiveRecord::Base
belongs_to :artist
end
... and in coffeescript (to handle deletions):
#ArtistCtrl = ($scope, $routeParams, $location, API) ->
$scope.destroyUrls = []
$scope.update = (artist) ->
artist.urls.push({id: id, _destroy: true}) for id in $scope.destroyUrls
artist.$update(redirectToShow, artistError)
$scope.deleteURL = (artist,url) ->
artist.urls.splice(artist.urls.indexOf(url),1)
$scope.destroyUrls.push(url.id)
Something is missing from all of the answers, which is the inputs for fields_for in the form.
The form works if you do this:
f.fields_for #model.submodel do ..
However, the form is sent as model[submodel], but that's what causes the error others have mentioned in their answers. If you try to do model.update(model_params), Rails will raise an error that it's expecting a Submodel type.
To fix this, make sure you follow the :name, value format:
f.fields_for :submodel, #model.submodel do ...
Then in the controller, make sure you put _attributes on your params:
def model_params
params.require(:model).permit(submodel_attributes: [:field])
end
Now the save, update, etc. will work fine.
Wasted several days trying to figure out how to use accepts_nested_attributes with Angular, and the issue is always the same: Rails whitelist will not allow the variables into the params hash. I've tried every single different whitelisting syntax that everyone said on SO and other blogs, tried using :inverse, tried using habtm and mas_many_through, tried manually rolling my own solution but that wont work if the whitelist wont allow params through, tried doing what http://guides.rubyonrails.org says about 'Outside the Scope of Strong Parameters', tried removing whitelisting all together which isnt really an option but it causes other problems anyways. Not sure why rails 4 strong parameter whitelisting wont allow arbitrary data thru, thats a huge problem especially if accepts_nested_attributes doesn't work either.... I guess we are left to just create/delete all associations on a separate page/form/controller and look like an idiot making my end users use several forms/pages to do something that should be easily doable on 1 page with 1 form. Ya know, usually I expect Angular to screw me, but this time Angular worked quite well and it was actually Rails 4 that screwed me twice on 1 issue that should be very straightforward.
I am trying to find all users that signed up during a given period of time to the ActionMovie plan. I am running into an N+1 problem and it's taking me a very long time to get the number of new signups. I was wondering if there was any creative thing I could do with arel_tables or something like that that could help cut down on this process?
My current code looks similar to the below:
#find all UserMovies created during time frame
user_movies = UserMovie.where(:created_at => start_time..end_time)
#find users
users = user_movies.collect {|um| um.user}
#iterate through each users user_movies and see if the their first action movie was during the time frame I am looking for
users.each do |user|
user_movies_array = user.user_movies.map {|um| {um.movie.type => um.created_at}}
user_movies_array.each do |um|
if um["ActionMovie"] > start_time
puts "new user"
end
end
end
Class User
has_many :user_movies
has_many :movies, :through => :user_movies
end
Class Movie
has_many :user_movies, :foreign_key => :movie_id
has_many :users, :through => :user_movies
end
Class UserMovie
belongs_to :user
belongs_to :movie
end
Class ActionMovie < Movies
end
Class SuspenseMovie < Movies
end
Have you tried eager loading the Movie association using the :include option?
Take a look at the API docs for #has_many to see specific implementation and scroll to the top to the section called Eager loading of associations to see a general overview.
I have a model named Song. I also have a model named Listen. A Listen belongs_to :song, and a song :has_many listens (can be listen to many times).
In my model I want to define a method self.top which should return the top 5 songs listened to the most. How can I achieve that using the has_many relation?
I'm using Rails 3.1.
Thanks!
Using named scopes:
class Song
has_many :listens
scope :top5,
select("songs.id, OTHER_ATTRS_YOU_NEED, count(listens.id) AS listens_count").
joins(:listens).
group("songs.id").
order("listens_count DESC").
limit(5)
Song.top5 # top 5 most listened songs
Even better, use counter_cache which will be faster because you'll only because using one table in your query
Here is your song class:
class Song < ActiveRecord::Base
has_many :listens
def self.top
order('listens_count DESC').limit(5)
end
end
Then, your listen class:
class Listen < ActiveRecord::Base
belongs_to :song, counter_cache: true
end
Make sure you add a migration:
add_column :comments, :likes_count, :integer, default: 0
Bonus points, add test:
describe '.top' do
it 'shows most listened songs first' do
song_one = create(:song)
song_three = create(:song, listens_count: 3)
song_two = create(:song, listens_count: 2)
popular_songs = Song.top
expect(popular_songs).to eq [song_three, song_two, song_one]
end
end
Or, if you want to go with the above method, here it is a bit more simply, and using a class method rather than scope
def self.top
select('comments.*, COUNT(listens.id) AS listens_count').
joins(:listens).
group('comments.id').
order('listens_count DESC').
limit(5)
end
For rails 4.x try this if your rows without any association matters:
scope :order_by_my_association, lambda {
select('comments.*, COUNT(listens.id) AS listens_total')
.joins("LEFT OUTER JOIN listens ON listens.comment_id = comments.id")
.group('comments.id')
.order("listens_total DESC")
}