Rails user to user messages

跟風遠走 提交于 2019-12-19 04:25:13

问题


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:

  1. Make sure you save your objects with the correct parents
  2. To display the "threaded" conversation, loop through those parents
  3. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!