I'm using Rails 4 and am having trouble figuring out how to set up my models so that one user can leave feedback for another user.
I have tried to follow the answer to the attached question: rails 4 users reviews for users how to do this
My models are: User, Profile, Feedback
The associations between models are:
User.rb
has_one :profile
has_many :feedbacks
accepts_nested_attributes_for :feedbacks
Profile.rb: belongs_to :user
Feedback.rb:
belongs_to :user
has_one :reviewer, :class_name => 'User', :foreign_key => 'reviewer'
The feedback table has attributes called :comment (text), :reviewer (integer) and :created_at(datetime). The reviewer needs to be the user id of the user who left the feedback.
The user model has attributes for :first_name and :last_name.
In my profile show page, I have a link to the feedback partial:
<%= render "feedbacks/feed" %>
That partial then has:
<div class="containerfluid">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<% if can? :read, #feedback && #user.feedback.comment.!blank %>
<%= #profile.user.feedbacks.each do |feedback|%>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<%= #feedback.comment %>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="logobox">
<%= "#{#user.feedback.reviewer.first_name} #{#user.feedback.reviewer.last_name}" %>
</div>
</div>
<div class="col-md-3">
<div class="logobox">
<%= #feedback.try(:created_at).try(:strftime, '%d %B %Y') %>
</div>
</div>
<% end %>
<% else %>
<%= render html: "No feedback available".html_safe %>
</div>
<% end %>
</div>
</div>
</div>
What I want from the above is to display the feedback to any user who can read the feedback on the user's (receiving the feedback) profile page. If there is no feedback or the current_user cannot view the feedback, then the else statement should be applied.
I have several problems with the above:
I don't know how to traverse models to link to the feedback. The feedback partial is being displayed in the profile show page. Feedback belongs to user and profile belongs to user.
I am getting an error pointing to the first line of the above (being <% if can? :read, #feedback && user.feedback.comment.!blank? %>. The error says it is expecting 'then'. I have never used 'then' before and it doesn't work when i just type that after blank.
I have an integer attribute in my feedbacks table called :reviewer. I want to store the user id of the user who created the feedback (on the other user) in that field. I don't know how to do this. The SO post copied above suggests I put a line in my feedback model which says: has_one :reviewer, :class_name => 'User', :foreign_key => 'reviewer'. I've tried this but I can't understand what it is doing.
Can anyone help? Perhaps there is another way to approach this problem and I would appreciate help finding it. The SO post I did manage to find was voted too broad, but I can't find any other references with more specific aspects of this problem set out.
My approach would have been using polymorphic associations. Create a polymorphic model.
class Feedback < ActiveRecord::Base
belongs_to :reviewer, polymorphic: true
belongs_to :reviewable, polymorphic: true
end
After my polymorphic model is set up I create the concerns which you can include as behaviours in your user model.
module Reviewer
extend ActiveSupport::Concern
included do
has_many :given_reviews, as: :reviewer, dependent: :destroy
end
end
module Reviewable
extend ActiveSupport::Concern
included do
has_many :received_reviews, as: :reviewable, dependent: :destroy
end
end
Your user model should look something like this.
class User
include Reviewer #user is behaving as a reviewer
include Reviewable #user is behaving as a reviewable entity
end
After all this is set up you have a reusable module to start with which will work with any other model as well.
userA.given_reviews #reviews posted by the user A
userA.received_reviews #reviews other people have given about user A
The feedback class is a join from user-reviewer. It should have two foreign keys on it. For user and reviewer. Make them both belongs_to.
User.rb
has_one :profile
has_many :feedbacks
has_many :reviewers, :through => :feedbacks, :class_name => 'User'
has_many :created_feedbacks, :class_name => 'Feedback', :foreign_key => 'reviewer_id'
has_many :reviewed_users, :through => :feedbacks_left, :source => :user, :class_name => 'User'
# Not sure if you need accepts nested attributes.
Feedback.rb:
belongs_to :user
belongs_to :reviewer, :class_name => 'User' # foreign_key should be reviewer_id
Note - if you use has_one or has_many, it expects the foreign key to be on the associating class. In the way you have it written up, that would mean the feedback_id is on the user, but that doesn't make sense. Make it a belongs_to instead.
Change reviewer to reviewer_id and add it to the feedback model. Then the reviewer method generated by the database column won't clobbered by the reviewer method that comes from the belongs_to.
After that, you'll be good.
Related
I've already setup a notification for whenever a user posts a review on a book, the book.user gets notified in this partial: views/notifications/_book.html.erb. At the same time, I trying to create notification to a user's followers for whenever the user they're following posts a new book. IMO, supposed to create a book partial to render the notification view but I already have a duplicate of the book partial to show reviews notifications. Now I don't know if I can still tweak in in the same file or create something else. I'm using https://github.com/rails-engine/notifications.
I've implemented following relationships which is working well on my app from this tutorial https://www.devwalks.com/lets-build-instagram-with-ruby-on-rails-part-6-follow-all-the-people/
This is what I've done so far with the codes
user.rb
has_many :books, dependent: :destroy
has_many :chapters, dependent: :destroy
has_many :reviews, dependent: :destroy
has_many :genres
has_many :ratings
review.rb
belongs_to :book
belongs_to :user
after_commit :create_notifications, on: :create
private
def create_notifications
Notification.create do |notification|
notification.notify_type = 'book'
notification.actor = self.user
notification.user = self.book.user
notification.target = self
notification.second_target = self.book
end
end
views/notifications/_book.html.erb
<div class=''>
<%= link_to notification.actor.username, main_app.profile_path(notification.actor.username) %> reviewed
<%= link_to notification.second_target.title, main_app.book_path(notification.second_target) %>
</div>
<div class=''>
<% unless notification.target.blank? %>
<div class="review-rating" data-score="<%= notification.target.rating %>"></div>
<%= notification.target.comment %>
<% end %>
</div>
book.rb
belongs_to :user
has_many :chapters, dependent: :destroy
after_commit :create_notifications, on: :create
def create_notifications
self.user.followers.each do |follower|
Notification.create(notify_type: 'book',
actor: self.user,
user: follower,
target: self,
second_target: self.book)
end
end
So, if I've to render in the same partial, how should I? or maybe I've to do it another way?
After lots of studying I got to know that a notify_type partial must be must be created which must also correspond to the book's notify_type.
i.e I created a
notify_type: "new_book"
Name of partial= '_new_book.html.erb'
I've read through many other topics here (1, 2, 3...) but none really solved my problem.
Here are my 3 models.
User
has_many :memberships
has_many :accounts, :through => :memberships
accepts_nested_attributes_for :memberships
end
Account
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships
end
Membership
attr_accessible :account_id, :url, :user_id
belongs_to :account
belongs_to :user
end
As you can see, my join model Membership has an additional attribute: :url.
In my Accounts table, I store names of online services, such as GitHub, Stack Overflow, Twitter, Facebook, LinkedIn.. I have 9 in total. It's a fixed amount of accounts that I don't tend to update very often.
In my User form, I'd like to create this:
The value entered in any of these field should be submitted in the Memberships table only, using 3 values:
url (the value entered in the text field)
user_id (the id of the current user form)
account_id (the id of the related account, e.g. LinkedIn is '5')
I have tried 3 options. They all work but only partially.
Option #1
<% for account in #accounts %>
<%= f.fields_for :memberships do |m| %>
<div class="field">
<%= m.label account.name %><br>
<%= m.text_field :url %>
</div>
<% end %>
<% end %>
I want to have 9 text field, one for each account. So I loop through my accounts, and create a url field related to my memberships model.
It shows my fields correctly on the first time, but the next time it'll display 81 fields:
Option #2
<% #accounts.each do |account| %>
<p>
<%= label_tag(account.name) %><br>
<%= text_field_tag("user[memberships_attributes][][url]") %>
<%= hidden_field_tag("user[memberships_attributes][][account_id]", account.id) %>
<%= hidden_field_tag("user[memberships_attributes][][user_id]", #user.id) %>
</p>
<% end %>
I'm trying to manually enter the 3 values in each column of my Memberships tables.
It works but :
displaying both account and user id's doesn't seem very secure (no?)
it will reset the fields everytime I edit my user
it will duplicate the values on each submit
Option #3 (best one yet)
<%= f.fields_for :memberships do |m| %>
<div class="field">
<%= m.label m.object.account.name %><br>
<%= m.text_field :url %>
</div>
<% end %>
I'm creating a nested form in my User form, for my Membership model.
It works almost perfectly:
exactly 9 fields, one for each account
no duplicates
But, it only works if my Memberships table is already populated! (Using Option #2 for example).
So I tried building some instances using the UsersController:
if (#user.memberships.empty?)
#user.memberships.build
end
But I still get this error for my m.label m.object.account.name line.
undefined method `name' for nil:NilClass
Anyway, I'm probably missing something here about has_many through models. I've managed to create has_and_belongs_to_many associations but here, I want to work on that join model (Membership), through the first model (User), using information about the third model (Account).
I'd appreciate your help. Thank you.
in the controller, fetch the list of memberships for a particular user
# controller
# no need to make this an instance variable since you're using fields_for in the view
# and we're building additional memberships later
memberships = #user.memberships
then loop through each account and build a membership if the user has no membership for an account yet.
# still in the controller
Account.find_each do |account|
unless memberships.detect { |m| m.account_id == account.id }
#user.memberships.build account_id: account.id
end
end
then in your view, you change nothing :)
I would use the following data-design approach. All users in your system should have the
memebership entries for all possible accounts. The active configurations will have a value for the url field.
User
has_many :memberships
has_many :accounts, :through => :memberships
has_many :active_accounts, :through => :memberships,
:source => :account, :conditions => "memberships.url IS NOT NULL"
accepts_nested_attributes_for :memberships
end
Now
curent_user.active_accounts # will return the accounts with configuration
curent_user.accounts # will return all possible accounts
Add a before_filter to initialize all the memberships that a user can have.
class UsersController
before_filter :initialize_memberships, :only => [:new, :edit]
private
def initialize_memberships
accounts = if #user.accounts.present?
Account.where("id NOT IN (?)", #user.account_ids)
else
Account.scoped
end
accounts.each do |account|
#user.memberships.build(:account_id => account.id)
end
end
end
In this scenario you need to initialize the memeberships before the new action and all the memberships should
be saved in the create action ( even the ones without url).
Your edit action doesn't need to perform any additional data massaging.
Note:
I am suggesting this approach as it makes the management of the form/data straight forward. It should only
be used if the number of Account's being associated is handful.
I have a database of skills that relate to each other as prerequisites to each other. In an index of skills, I'd like to be able to search through other skills and add 1 or more as prerequisites. It's important to note that I ONLY want the user to be able to add prerequisites, not remove them, as that's taken care of through an up-down voting system. I'm using JQuery Tokeninput and actually have all of this working except for one thing: I can't figure out how to only add prerequisites, rather than replacing all the prerequisites for a particular skill on submit.
Models:
class Skill < ActiveRecord::Base
attr_accessible :skill_relationship_attributes, :prereq_tokens
has_many :skill_relationships
has_many :prereqs, :through => :skill_relationships
has_many :inverse_skill_relationships, :class_name => 'SkillRelationship', :foreign_key => "prereq_id"
has_many :inverse_prereqs, :through => :inverse_skill_relationships, :source => :skill
attr_reader :prereq_tokens
accepts_nested_attributes_for :skill_relationships, :allow_destroy => true
def prereq_tokens=(ids)
self.prereq_ids = ids.split(",")
end
end
class SkillRelationship < ActiveRecord::Base
attr_accessible :skill_id, :prereq_id, :skill_attributes, :prereq_attributes
belongs_to :skill
belongs_to :prereq, :class_name => 'Skill'
end
JQuery:
$('#skill_prereq_tokens').tokenInput('/skills.json',
{ theme:'facebook',
propertyToSearch:'title',
queryParam:'search',
preventDuplicates:'true'
});
View:
<%= simple_form_for skill do |f| %>
<%= f.input :prereq_tokens %>
<%= f.submit %>
<% end %>
I feel a bit silly for not getting this before, but I solved my problem by changing how prereq_tokens became prereq_ids in my Skill model.
I just changed this:
def prereq_tokens=(ids)
self.prereq_ids = ids.split(",")
end
to this:
def prereq_tokens=(ids)
self.prereq_ids += ids.split(",")
end
That's it. That little plus sign before the equals sign. I hope this helps anyone else who codes too long without a break!
I am new to rails, and am trying to set up a many-to-many relationship in my rails project. I have a small strategy, but I am not sure if its the correct way.
Aim:
I have a table of users, and a table of groups. Users can be part of many groups, and each group may have many users.
Strategy:
Set up User migration to have name:string
Set up Group migration to have name:string
Set up a Join table migration
Set up User model such that it would have has_and_belongs_to_many :groups
Set up Group model such that it would have has_and_belongs_to_many :users
Would this be the correct strategy? Thanks!
Railcast Summary from answer:
For those that are interested - Railcast suggests you to use a has_many :through association since the strategy above has the limitation that you cannot add extra relation-specific information.
check out: http://kconrails.com/tag/has_many/
First, I assume, you have a user-model with a field "name" and a group-model with a field "name".
You need a model between users and groups. Let's call it grouping:
rails g model grouping user_name:string group_name:string
In the grouping-model (grouping.rb), you put:
belongs_to :user
belongs_to :group
In the user-model:
has_many :groupings, :dependent => :destroy
has_many :groups, :through => :groupings
And in the group-model:
has_many :groupings, :dependent => :destroy
has_many :users, :through => :groupings
In the _form file to edit or update a user's profile, you put:
<div class="field">
<%= f.label :group_names, "Groups" %>
<%= f.text_field :group_names %>
</div>
And, finally, the User-class must know, what to do with the information from the form. Insert into user.rb:
attr_writer :group_names
after_save :assign_groups
def group_names
#group_names || groups.map(&:name).join(' ')
end
private
def assign_groups
if #group_names
self.groups = #group_names.split(/\,/).map do |name|
if name[0..0]==" "
name=name.strip
end
name=name.downcase
Group.find_or_create_by_name(name)
end
end
end
assign_groups removes whitespace and downcases all words, so you won't have redundant tags.
Now, you can show the groups for a user in the show file of his or her profile:
<p>Groups:
<% #user.groups.each do |group|%>
<%= group.name %>
<% end %>
</p>
Hope, that helps.
i have a User model and a UserMessage model (a model for holding the private messages between two users)
in my view i have..
<% if #message_items.any? %>
<ol class="messages">
<%= render partial: 'message_item', collection: #message_items%>
</ol>
<%= will_paginate #message_items %>
<% end %>
which i render with...
<li id="<%= message_item.id %>">
<span class="user">
<%= link_to message_item.user.name, message_item.user %>
</span>
<span>
<%= message_item.title %>
</span>
<span>
<%= message_item.body %>
</span>
</li>
how is the object UserMessage(which is coming from message_item) able to render the User object? my design for the UserMessage just has the following attributes "id, user_id, from_id, title, body, created_at, updated_at".
i guess its from the user_id, and rails somehow makes the connection and is able to find the User object from the user_id. is that correct?
but i what i really want though, is the user from the from_id (the person sending the message). is there a way to retrieve that? i know doing something like.. message_item.user.from_id does not work.
the only way i could think of that works is by doing
<%= User.find(id= message_item.from_id).name %>
but that doesn't seem right putting so much code in my view. sorry but ive been super stuck. help would be much appreciated. thanks
You need the following models
class User < ActiveRecord::Base
has_many :received_user_messages,
:class_name => "UserMessage", :foreign_key => :receiver_id
has_many :sent_user_messages, :class_name => "UserMessage",
:class_name => "UserMessage", :foreign_key => :sender_id
end
class UserMessage < ActiveRecord::Base
# should have sender_id and receiver_id columns
# make sure you index these columns
belongs_to :sender, :class_name => "User"
belongs_to :receiver, :class_name => "User"
end
Do the following, to list the messages received by an an user:
current_user.received_messages(:include => :sender).each do |message|
p "[#{message.sender.name}]: #{message.content}"
end
Do the following, to list the messages sent by an an user:
current_user.sent_messages(:include => :receiver).each do |message|
p "[#{message.receiver.name}]: #{message.content}"
end
i think you are looking for foreign_key option for belongs_to in model. So what you need is to specify something like sender/from relation with from_id in UserMessage message
belongs_to :sender, foreign_key: :from_id, class_name: "User"
Then in template you just call this relation in view.
message_item.sender
It should work same as message_item.user.
For further reference visit documentation for associations
In addition i recommend you not call .name method in template, but specify to_s method in your model. Good approach pointed by klump in his answer is to use .include method for better performance. It will load user data while loading UserMessage data, not in another query.
Article.find :all, :include => :revisions
Code was derived from another answer — Rails ActiveRecord: Is a combined :include and :conditions query possible?
Try this:
In your UserMessage class, make two class methods named sender and recipient that perform the complex queries you want. Then use these methods in your view.
First you need to set the associations. I guess that one user has many messages and one message belongs to one user.
add to the models:
app/models/user.rb
has_many :user_messages
*app/models/user_messages.rb*
belongs_to :user
You might need to add a column to your user_messages table, called user_id
When you fetch the messages in the controller, tell rails to load the associated user right away, so rails doesnt have to do this later on:
#message_items = UserMessage.includes( :user ).all
Now you can access the user object "owning" the message really easy:
<%= message_item.user.name %>
If you need all the messages owned by a user this also is easy now:
#user = User.find( some_id )
#user.messages # this returns an array with all the message objects