问题
I'm very new to rails so please be detailed in your responses. I'm building a web app that uses devise for authentication. The part that I'm stuck on right now is a user to user messaging system. The idea is that User A logs into the app and can visit user B's profile, and on User B's profile can click on a link that allows User A to compose a message to User B. Then User B can log into the app and visit the inbox where User A's message will be found.
I believe that I'm having trouble defining the sender and recipient roles here, right now I'm trying to display the form that users will compose their message in. Can anyone see what I'm doing wrong here? I get the following error. I've read that the thing to do is add the User_id field to the table, but I'm hoping to link this messages up using sender_id and recipient_id, which both equal user_id (e.g. User 1[sender] sends a message to User 2 [recipient]):
unknown attribute: user_id
def new @message = current_user.messages.new recipient_id: params[:sender_id] end
Additionally, for you rails experts or anyone that has done something similar to this, can you advise whether or not I'm going in the right direction, or offer any guidance? I'm sort of coding blind here and just trying to make it up as I go along. Any guidance would be hugely appreciated and save me a lot of time i'm sure. Code below:
Users Migration
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
t.string :first_name
t.string :last_name
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :reset_password_token
t.datetime :reset_password_sent_at
t.datetime :remember_created_at
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end
end
Messages Migration
class CreateMessages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.string :content
t.integer :sender_id
t.integer :recipient_id
t.timestamps
end
end
end
schema.rb
ActiveRecord::Schema.define(version: 20140909174718) do
create_table "messages", force: true do |t|
t.string "content"
t.integer "sender_id"
t.integer "recipient_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", force: true do |t|
t.string "first_name"
t.string "last_name"
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.string "current_industry"
t.integer "years_in_current_industry"
t.string "hobbies"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
routes.rb
Catalyst::Application.routes.draw do
devise_for :users, :controllers => { :registrations => "registrations" }
devise_scope :user do
get 'register', to: 'devise/registrations#new'
get 'login', to: 'devise/sessions#new', as: :login
get 'logout', to: 'devise/sessions#destroy', as: :logout
end
resources :users do
member do
get 'edit_profile'
end
resources :messages, only: [:new, :create]
end
resources :messages, only: [:index, :show, :destroy]
root to: "home#index"
match '/about', to: 'static_pages#about', via: 'get'
match '/contact', to: 'static_pages#contact', via: 'get'
match '/help', to: 'static_pages#help', via: 'get'
match '/legal', to: 'static_pages#legal', via: 'get'
end
users_controller
class UsersController < ApplicationController
before_filter :authenticate_user!
def index
@users = User.all
end
def show
@user = User.find(params[:id])
end
def new
end
def create
end
def edit
end
def update
@user = User.find(params[:id])
@user.update!(user_params)
redirect_to @user
end
def destroy
end
def edit_profile
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :current_industry, :years_in_current_industry, :hobbies)
end
def sender
@user = User.find(params[:id])
end
def recipient
@user = User.find(params[:id])
end
end
messages_controller
class MessagesController < ApplicationController
before_action :set_recipient
def new
@message = Message.new
@recipient = User.find(params[:user_id])
end
def create
@message = Message.new message_params
if @message.save
flash[:success] = "Your message has been sent!"
redirect_to user_messages_path
else
flash[:failure] = "Please try again."
redirect_to users_path
end
end
private
def message_params
params.require(:message).permit(:content, :sender_id, :recipient_id)
end
end
user.rb
class User < ActiveRecord::Base
has_many :from_messages, class_name: 'Message', :foreign_key => "sender_id"
has_many :to_messages, class_name: 'Message', :foreign_key => "recipient_id"
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :first_name, :last_name, :email, :password, :password_confirmation, :remember_me, :current_industry, :years_in_current_industry, :hobbies
end
message.rb
class Message < ActiveRecord::Base
belongs_to :sender, class_name: "User"
belongs_to :recipient, class_name: "User"
validates :content, presence: true, length: { maximum: 500 }
validates :sender_id, presence: true
validates :recipient_id, presence: true
end
messages/index.html.erb
<h2>Inbox</h2>
messages/new.html.erb
<h1>Create Message</h1>
<%= form_for [@recipient, @message] do |f| %>
<%= f.hidden_field :recipient_id, value: @recipient.id %>
<%= f.label "Enter your message below" %><br />
<%= f.text_area :content %>
<%= f.submit "Send" %>
<% end %>
rake routes
user_messages POST /users/:user_id/messages(.:format) messages#create
new_user_message GET /users/:user_id/messages/new(.:format) messages#new
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
messages GET /messages(.:format) messages#index
message GET /messages/:id(.:format) messages#show
DELETE /messages/:id(.:format) messages#destroy
回答1:
Models
#app/models/user.rb
class User < ActiveRecord::Base
has_many :messages, class_name: "Message", foreign_key: "recipient_id"
has_many :sent_messages, class_name: "Message", foreign_key: "sender_id"
end
#app/models/message.rb
class Message < ActiveRecord::Base
belongs_to :recipient, class_name: "User", foreign_key: "recipient_id"
belongs_to :sender, class_name: "User", foreign_key: "sender_id"
scope :unread, -> { where read: false }
end
This should give you the ability to create messages which "belong" to a user (IE the recipient), and then you can associate a "sender" profile to those messages.
--
Controllers
This will give you the ability to call the following:
#app/controllers/messages_controller.rb
class MessagesController < ApplicationController
before_action :set_recipient, only: [:new, :create]
def new
@message = current_user.sent_messages.new
end
def create
@message = current_user.sent_messages.new message_params
@message.recipient_id = @recipient.id
@message.save
end
def index
@messages = current_user.messages
end
def destroy
@message = current_user.messages.destroy params[:id]
end
def show
@message = current_user.messages.find params[:id]
end
private
def message_params
params.require(:message).permit(:content, :recipient_id, :sender_id)
end
def set_recipient
@recipient = User.find params[:user_id]
end
end
--
Routes
#config/routes.rb
devise_for :users, path: "", controllers: { :registrations => "registrations" }, path_names: {sign_up: "register", sign_in: "login", sign_out: "logout"}
resources :users do
get :profile
resources :messages, only: [:new, :create] #-> domain.com/users/:user_id/messages/new
end
resources :messages, only: [:index, :show, :destroy] #-> domain.com/messages/:id
--
Views
This will give you the ability to use the following links:
#app/views/users/show.html.erb (user to send message to)
<%= link_to "Send Message", user_messages_path(@user.id) %>
#app/views/messages/new.html.erb
<%= form_for [@recipient, @user] do |f| %>
<%= f.text_field :content %>
<%= f.submit %>
<% end %>
#app/views/messages/index.html.erb
<h2>Inbox</h2>
<% @messages.each do |message| %>
<%= message.content %>
<% end %>
--
Fix
I've read that the thing to do is add the User_id field to the table, but I'm hoping to link this messages up using sender_id and recipient_id, which both equal user_id (e.g. User 1[sender] sends a message to User 2 [recipient])
You don't need to add user_id
to your table. user_id
is merely a foreign_key, which you've overridden in your models
.
All you need to do is set the recipient_id
and sender_id
, which we're doing in the create
method:
def create
@message = current_user.message.new message_params
@message.recipient_id = @recipient.id
@message.save
end
You've done some very clever things here.
Firstly, you have implicitly set the sender_id
foreign key by calling current_user.messages
. If you had called Message.new
, it would have been a completely different story (having to set sender_id
)
Secondly, because you're using nested routes, you'll be able to use the @recipient
variable you've set in the before_action
method to give us the id
for the recipient_id
.
This should work for you. You won't need to use inverse_of
unless you are trying to access "parent" model data in a child / nested model.
Recommendations
What you're doing is completely valid
The core trick is to make sure your Message
model is completely separate & independent to your User
. This is achieved with your setup, allowing you to create the various objects that you require.
The other aspect you need to consider is how you're going to ensure you're able to provide the users with the ability to have "threaded" messages. You'll achieve this using one of the hierarchy gems, either Ancestry or Closure_Tree
Adding this functionality will be a little more in-depth. I can provide information if you require (just leave a comment)
Threading
The hierarchy gems are actually relatively simple to use.
The trick to "treading" your messages is to use one of these gems (either Ancestry
or Closure_Tree
), as they provide you with "methods" which you can call on your items. They work by creating several columns in your database, populating them as you save / create the objects you desire
The "threading" issue is a big one, as without the "hierarchy" gems, you won't be able to call the "child" objects of the record you want, thus preventing the threading from occurring. Here's a good Railscast on how to achieve it:
The trick with this is to use something called "recursion"
Recursion is where you create an "indefinite" loop, so far as how "recursive" the data is. EG if you have an object with children, you'll have to cycle through the children, and then the children of those children, recursively until you reach the point of showing all the data:
Recursion is the process of repeating items in a self-similar way. For instance, when the surfaces of two mirrors are exactly parallel with each other, the nested images that occur are a form of infinite recursion.
As such, here's how you to it:
- Make sure you save your objects with the correct parents
- To display the "threaded" conversation, loop through those parents
- Use recursion to loop through their children
We use the ancestry gem, which stores the hierarchy slightly differently to the closure_tree
gem we've since discovered (intend to use the closure tree gem soon).
You firstly have to therefore save any hierarchy yourself:
This will allow you to save the various "parents" for that object. This means that when you load the object, and wish to cycle through its descendent, you'll be able to use the Ancestry object methods:
Which means you'll be able to use the following:
#app/views/comments/index.html.erb
<%= render partial: "comments", locals: { collection: @comments } %>
#app/comments/_comments.html.erb
<% collection.arrange.each do |comment, sub_item| %>
<%= link_to comment.title, comment_path(comment) %>
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
<% end %>
回答2:
To solve the error you have, try to set :inverse_of
attribute of has_many
and belongs_to
statements in your model classes. You can end up having two has_many
- one per each belongs_to
reverse:
user.rb:
has_many :from_messages, :class_name => 'Message', :foreign_key => "sender_id", :inverse_of => :sender
has_many :to_messages, :class_name => 'Message', :foreign_key => "to_id", :inverse_of => :recipient
message.rb:
belongs_to :sender, :class_name => 'User', :inverse_of => :from_messages
belongs_to :recipient, :class_name => 'User',:inverse_of => :to_messages
Overall I think your approach is a good starting point for a messaging system. You can try to post your code to https://codereview.stackexchange.com/ for a detailed review.
来源:https://stackoverflow.com/questions/25752438/rails-user-to-user-messages