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.
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.
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.
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
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
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.
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.
8.5 hours from 10/31 to 11/6.
Recommended Posts