I tried to make a message function of Rails Tutorial extension (Part 1): Create a model

This is a continuation of Creating Extensions in Chapter 14 of the Rails Tutorial.

The reply function has been completed up to the last time. The second function addition is to create a message function.

Investigate functional requirements

For the tutorial

Twitter supports the ability to send direct messages. Let's implement this feature in a sample application

Since there is, check the function of Twitter.

(Hint: You'll need a Message model and a regular expression that matches the new micropost).

Is the Message model a Rails feature? I will look it up on the net.

First, check the function of Twitter.

The other person must be following you, provided that you can send a DM. I'll create this feature later.

There is also a function to display read, but this function is given up.

You can set to receive push notification / SMS notification (short mail notification) / mail notification when you receive DM. I will give up this function as well.

You can also mute DMs from specific accounts to prevent them from receiving notifications. I will give up this function as well.

There is also a function to deny DM from a specific account. You can block your opponent. I will give up this function as well.

You can delete the received DM. I will make this function. The sender cannot delete the DM after it has been sent.

DM and post have different screens.

When I investigated whether the maximum number of characters was different, it was 140 characters until July 2015 and 10,000 characters after that. This time I will use 140 characters.

After investigating whether multiple destinations can be used, there was a function to create a group and talk with multiple accounts. It is said that it was completed in January 2015, so I will give up this function.

I understood the function of Twitter, and I was able to imagine the function to be created this time.

I investigated whether the Message model is a function of Rails, but I could not find such an article. I found an article to make a chat function by myself and an article to make a real-time chat, but it seems to be irrelevant.

Summary of functions

  1. You can send a message.
  2. Only one recipient, not multiple recipients.
  3. Up to 140 characters.
  4. You can send to anyone without any restrictions. This restriction function will be created later.
  5. Do not create a read display function.
  6. Do not create a notification function when receiving DM.
  7. You can delete the received DM.
  8. The sender cannot delete the sent DM

Model design

Make a model specification. Reread the tutorial where you make a Micropost. In Chapter 13, we first make a model in 13.1. I will make a model of DM in the same way.

Column name attribute
id integer
content text
sender_id integer
receiver_id integer
created_at datetime
updated_at datetime

Figure DM model

Here the receiver is also related to the User model. I think that this relationship is the same as when I made the follow model, so I reread the tutorial.

Consider specifications when deleting user

In relation, deleting user also deletes relation. I decided not to delete it in DM. This is different.

In relation, unfollow also removes relation. In DM, even if the recipient deletes the DM, the DM does not disappear and does not disappear from the sender's screen. It just disappears from the recipient's screen. Let's think about what this means. Deletion of DM does not mean that it was not done, but the fact that it was sent remains. Like email, when the recipient deletes the received email, the sender's sent email is not deleted. I tried it on Twitter and it was certain.

Think of a bucket with DM, like an email. A model in which the sender and receiver have separate buckets. When you send a DM, you put one in the sender's bucket and one in the recipient's bucket. I felt that putting the exact same message in two buckets was a duplicate.

So I'll keep one bucket and add a delete flag so that the recipient knows it's deleted.

I searched the net for attributes to put true or false and found that there was a boolean and that I should always put the default value. The reason is that nil and false are treated the same in ruby. https://qiita.com/jnchito/items/a342b64cd998e5c4ef3d

It is a model after the change.

Column name attribute
id integer
content text
sender_id integer
receiver_id integer
deleted boolean
created_at datetime
updated_at datetime

Figure DM model

DM model development

Create a topic branch.

ubuntu:~/environment/sample_app (master) $ git checkout -b create-dm

Generate a dm model.

ubuntu:~/environment/sample_app (create-dm) $ rails generate model dm content:text user:references
      create    db/migrate/20201102003220_create_dms.rb
      create    app/models/dm.rb
      create      test/models/dm_test.rb
      create      test/fixtures/dms.ymlrails generate mode dm content:text user:references

Change the migration. I created the index because there are two possible ways, one is to specify the sender and retrieve it in chronological order, and the other is to specify the receiver to retrieve it in chronological order. For the handling of null of the deleted flag, I referred to the article on the net earlier.

db/migrate/20201102003220_create_dms.rb


class CreateDms < ActiveRecord::Migration[5.1]
  def change
    create_table :dms do |t|
      t.text :content
      t.integer :sender_id
      t.integer :reciever_id
      t.boolean :deleted, default: false, null: false

      t.timestamps
    end
    add_index :dms, [:sender_id, :created_at]
    add_index :dms, [:receiver_id, :created_at]
  end
end

Update the database.

ubuntu:~/environment/sample_app (create-dm) $ rails db:migrate

Associate User and DM. Read 14.1.2 "User / Relationship Association" in the tutorial.

app/models/user.rb


class User < ApplicationRecord
  has_many      :microposts, dependent: :destroy
  has_many      :active_relationships,class_name:  "Relationship",
                                      foreign_key: "follower_id",
                                      dependent:    :destroy
  has_many      :passive_relationships, class_name: "Relationship",
                                        foreign_key: "followed_id",
                                        dependent:  :destroy
  has_many      :following, through: :active_relationships, source: :followed                    
  has_many      :followers, through: :passive_relationships, source: :follower
  has_many      :sent_dms,class_name: "Dm",
                          foreign_key: "sender_id"
  has_many      :received_dms,class_name: "Dm",
                              foreign_key: "receiver_id"

app/models/dm.rb


class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
end

Draw a diagram to organize the relationships.

id name
1 Michael
2 Archer

user model

has_many

sender_id receiver_id content
1 2 ...
1 3 ...

dm model

has_many

id name
2 Archer
3 ...

user model Figure User and DM relationship

Methods that can be used

The methods that will be available are:

Method Use
user.sent_dms Returns a set of DMs sent by User
sent_dms.sender Returns sender
sent_dms.receiver Returns receiver
user.sent_dms.create(receiver_id: other_user.id) Create DM by linking with user
user.sent_dms.create!(receiver_id: other_user.id) Create DM by linking with user (output error when failing)
user.sent_dms.build(receiver_id: other_user.id) Returns a new DM object associated with user
user.sent_dms.find_by(id:1) Returns a DM with id 1 associated with user

Try it on the console. Make dm1.

>> user1 = User.first
>> user2 = User.second
>> dm1 = user1.sent_dms.create(receiver_id: user2.id, content: "hoge dm1")

User objects of sender and receiver are returned.

>> dm1.sender
=> #<User id: 1, name: "Example User", email: "[email protected]", created_at: "2020-10-26 01:37:04", updated_at: "2020-10-26 01:37:04", password_digest: "$2a$10$2TZtcwmSTCfl9Bigz2nYGO8U1YA8ksfNXUr2O/fSGOY...", remember_digest: nil, admin: true, activation_digest: "$2a$10$EaQUKa6hfGEHosjnICR4VuYMxfOxunTOsPGQYUimNLn...", activated: true, activated_at: "2020-10-26 01:37:03", reset_digest: nil, reset_sent_at: nil, unique_name: "Example">
>> dm1.receiver
=> #<User id: 2, name: "Van Zemlak", email: "[email protected]", created_at: "2020-10-26 01:37:04", updated_at: "2020-10-26 01:37:04", password_digest: "$2a$10$H22BJeNVA3hYdEw/a5RArekRy73q/0AtvidwRiVpoUK...", remember_digest: nil, admin: false, activation_digest: "$2a$10$xm7AJE4Q3fzq3gi5tmVnyeld8wahxMHN/dE2Sn2jSUW...", activated: true, activated_at: "2020-10-26 01:37:04", reset_digest: nil, reset_sent_at: nil, unique_name: "Craig1">

Search the list of DMs for user, search for dm by id.

>> user1.sent_dms
  Dm Load (0.2ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."sender_id" = ? LIMIT ?  [["sender_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

>> user1.sent_dms.find_by(receiver_id: 2)
  Dm Load (0.4ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."sender_id" = ? AND "dms"."receiver_id" = ? LIMIT ?  [["sender_id", 1], ["receiver_id", 2], ["LIMIT", 1]]
=> #<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">

I need a list of DMs for reciever. Read the tutorial because followed has the same structure. The method is likely, so I'll try it on the console.

>> user2.received_dms
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE "dms"."receiver_id" = ? LIMIT ?  [["receiver_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

I have a model, but I am ambiguous that I have the methods required for the requirements.

Review the Twitter screen again. The DM screen has a parent-child structure, and the parent screen is a list of users who have interacted with each other in the past. When you select a user, it is a screen that lists DM exchanges.

A required method is a method that returns a list of users with whom you have interacted in the past. When I searched find on the net, I found that the OR condition can be used in where. https://qiita.com/nakayuu07/items/3d5e2f8784b6f18186f2 Try it on the console.

>> Dm.where(sender_id: 1).or(Dm.where(receiver_id: 1))                                                                                         
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE ("dms"."sender_id" = ? OR "dms"."receiver_id" = ?) LIMIT ?  [["sender_id", 1], ["receiver_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>
>> Dm.where(sender_id: 2).or(Dm.where(receiver_id: 2))                                                                                        
  Dm Load (0.1ms)  SELECT  "dms".* FROM "dms" WHERE ("dms"."sender_id" = ? OR "dms"."receiver_id" = ?) LIMIT ?  [["sender_id", 2], ["receiver_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Dm id: 2, content: "hogehoge", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-02 02:27:35", updated_at: "2020-11-02 02:27:35">, #<Dm id: 3, content: "hoge dm1", sender_id: 1, receiver_id: 2, deleted: false, created_at: "2020-11-03 00:21:40", updated_at: "2020-11-03 00:21:40">]>

This search does not allow the function of the parent-child screen. All the DMs of all the opponents are mixed. At this point, it turned out that the specifications were not fully explored. The function to create a screen for each DM partner will be promoted if it can be created later.

Model testing

Make a test of the model. Make DM validation with reference to Micropost. Read 13.1.2 "Validation of Micropost" in the tutorial. The DM file of fixture is a sample, so delete it.

test/models/dm_test.rb


class DmTest < ActiveSupport::TestCase

  def setup
    @sender   = users(:michael)
    @receiver = users(:archer)
    @dm = Dm.new(content: "hogehoge1", sender_id: @sender.id, receiver_id: @receiver.id)
  end

  test "should be valid" do
    assert @dm.valid?
  end
  
  test "sender should be present" do
    @dm.sender_id = nil
    assert_not @dm.valid?
  end

  test "receiver should be present" do
    @dm.receiver_id = nil
    assert_not @dm.valid?
  end

  test "contentr should be present" do
    @dm.content = nil
    assert_not @dm.valid?
  end

  test "contentr should be at most 140 characters" do
    @dm.content = "a" * 141
    assert_not @dm.valid?
  end

end

Add validation for the same as micropost. The test is now GREEN.

app/models/dm.rb


class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
  validates :content, presence: true, length: { maximum: 140 }
end

Change the method when creating a DM to the customary correct way.

test/models/dm_test.rb


  def setup

    @dm = @sender.sent_dms.build(content: "hogehoge1", receiver_id: @receiver.id)
  end

Makes DMs return in chronological order. Read 13.1.4 "Improvement of Microposts". Write the test first.

test/models/dm_test.rb


class DmTest < ActiveSupport::TestCase
...
  test "order should be most recent first" do
    assert_equal sent_dms(:most_recent), Dm.first
  end
end

Create parent-child relationship data in the fixture. https://qiita.com/seimiyajun/items/ffefdfc74b9fce76a538 I referred to.

test/fixtures/dms.yml


morning:
  content: "Good morning!"
  sender:   michael
  receiver: archer
  created_at:  <%= 10.minutes.ago %>

Set so that they are arranged in the order of created_at. The test is now GREEN.

app/models/dm.rb


class Dm < ApplicationRecord
  belongs_to :sender,   class_name: "User"
  belongs_to :receiver, class_name: "User"
  default_scope -> { order(created_at: :desc) }
  validates :content, presence: true, length: { maximum: 140 }
end

Think about adding: destroy. When the user was deleted, the DM in the past was decided to remain. Add that test. Refer to Listing 13.20.

test/models/user_test.rb GREEN


test "associated dms should not be destroyed" do
    @user.save
    @user.sent_dms.create!(content: "Lorem ipsum", receiver_id: users(:archer).id)
    assert_no_difference 'Dm.count' do
      @user.destroy
    end
  end
end

The model has been completed so far.

Time required

8.5 hours from 10/31 to 11/6.

Recommended Posts

I tried to make a message function of Rails Tutorial extension (Part 1): Create a model
I tried to make a message function of Rails Tutorial extension (Part 2): Create a screen to display
I tried to make a reply function of Rails Tutorial extension (Part 3): Corrected a misunderstanding of specifications
Rails Tutorial Extension: I tried to create an RSS feed function
I made a reply function for Rails Tutorial extension (Part 2): Change model
I made a reply function for the Rails Tutorial extension (Part 1)
I made a reply function for the Rails Tutorial extension (Part 5):
I tried to make a group function (bulletin board) with Rails
[Rails] Implementation of multi-layer category function using ancestry "I tried to make a window with Bootstrap 3"
I tried to make a login function in Java
Rails Tutorial Extension: I created a follower notification function
[Rails / JavaScript / Ajax] I tried to create a like function in two ways.
Tutorial to create a blog with Rails for beginners Part 1
[Rails] I tried to create a mini app with FullCalendar
I want to make a specific model of ActiveRecord ReadOnly
I tried using Hotwire to make Rails 6.1 scaffold a SPA
Tutorial to create a blog with Rails for beginners Part 2
I tried to make a client of RESAS-API in Java
Tutorial to create a blog with Rails for beginners Part 0
I made a reply function for the Rails Tutorial extension (Part 4): A function that makes the user unique
I tried to implement Ajax processing of like function in Rails
I tried to make a machine learning application with Dash (+ Docker) part2 ~ Basic way of writing Dash ~
I tried to make a parent class of a value object in Ruby
I tried to create a LINE clone app
How to make a follow function in Rails
I tried to make a machine learning application with Dash (+ Docker) part3 ~ Practice ~
I tried to implement a function equivalent to Felica Lite with HCE-F of Android
Rails6 I want to make an array of values with a check box
I tried to create a Clova skill in Java
I want to define a function in Rails Console
I tried to create a shopping site administrator function / screen with Java and Spring
[Rails] Implementation of tutorial function
I tried to create a java8 development environment with Chocolatey
I want to implement a product information editing function ~ part1 ~
[Swift] I tried to implement the function of the vending machine
I want to make a function with kotlin and java!
I want to create a form to select the [Rails] category
A series of steps to create portfolio deliverables with Rails
[Rails] I tried to implement "Like function" using rails and js
I tried to create a padrino development environment with Docker
I tried to make the sample application into a microservice according to the idea of the book "Microservice Architecture".
[Rails 6.0, Docker] I tried to summarize the Docker environment construction and commands necessary to create a portfolio
[Ruby on Rails] Since I finished all Rails Tutorial, I tried to implement an additional "stock function"
Preparing to create a Rails application
I want to add a browsing function with ruby on rails
I tried to implement the image preview function with Rails / jQuery
I tried to create a Spring MVC development environment on Mac
[Unity] I tried to make a native plug-in UniNWPathMonitor using NWPathMonitor
I tried to build a simple application using Dockder + Rails Scaffold
[Java] I tried to make a maze by the digging method ♪
(Ruby on Rails6) Create a function to edit the posted content
Using the face detection function of Watson Visual Recognition, I tried to process a whole body image of a person into an image of only the face part
I tried to make a machine learning application with Dash (+ Docker) part1 ~ Environment construction and operation check ~
I tried to make a sample program using the problem of database specialist in Domain Driven Design
I tried to make a simple face recognition Android application using OpenCV
[Rails] I want to send data of different models in a form
[Rails] I tried to implement a transaction that combines multiple DB processes
A note of someone who stumbled when trying to create a Rails project
[iOS] I tried to make a processing application like Instagram with Swift
I tried to make a Web API that connects to DB with Quarkus
I made a virtual currency arbitrage bot and tried to make money