davidm
davidm

Reputation: 89

How to implement retweet functionality in RoR?

I'm trying to implement retweet functionality on my app.

So I have my retweet_id in my tweets model

tweets schema

| user_id | content | created_at | updated_at | retweet_id

tweets.rb

belongs_to :user
has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'

user.rb

has_many :tweets

And in my tweets controller

tweets_controller.rb

    ...


def retweet
    @retweet = Tweet.new(retweet_params)
      if @retweet.save
        redirect_to tweet_path, alert: 'Retweeted!'
      else
        redirect_to root_path, alert: 'Can not retweet'
      end
    end
    
    Private
    ...
    def retweet_params
      params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
    end

In my view

tweets/show.html.erb

<%= link_to 'Retweet', retweet_tweet_path(@tweet.id), method: :post %>

My routes

resources :tweets do
  resources :comments
  resources :likes
  member do
    post :retweet
  end
end

So when I try this I get an error

param is missing or the value is empty: retweet

So I remove .require from 'retweet_params' and that removes that error (though i'm unsure of how wise that is)

Then the link works but won't retweet - reverting to the fallback root_path specified in my action instead.

Unpermitted parameters: :_method, :authenticity_token, :id
Redirected to http://localhost:3000/

I'm not sure what i'm doing wrong. How can I get my retweets working? ty

Upvotes: 0

Views: 371

Answers (2)

3limin4t0r
3limin4t0r

Reputation: 21130

The reason retweet_params raises an error is because your link link_to 'Retweet', retweet_tweet_path(@tweet.id), method: :post doesn't contain parameters like a new or edit form does. Instead you should create a new tweet that reference to tweet you want to retweet.

before_action :set_tweet, only: %i[show edit update destroy retweet]

def retweet
  retweet = @tweet.retweets.build(user: current_user)
  if retweet.save
    redirect_to retweet, notice: 'Retweeted!'
  else
    redirect_to root_path, alert: 'Can not retweet'
  end
end

private

def set_tweet
  @tweet = Tweet.find(params[:id])
end

The above should automatically link the new tweet to the "parent". If this doesn't work for some reason you could manually set it by changing the above to:

retrweet = Tweet.new(retweet_id: @tweet.id, user: current_user)

The above approach doesn't save any content, since this is a retweet.

If you don't want to allow multiple retweets of the same tweet by the same user, make sure you have the appropriate constraints and validations set.

# migration
add_index :tweets, %i[user_id retweet_id], unique: true

# model
validates :retweet_id, uniqueness: { scope: :user_id }

How do we access the content of a retweet? The answer is we get the content form the parent or source (however you want to call it).

There is currently no association that lets you access the parent or source tweet. You currently already have:

has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'

To easily access the source content let's first add an additional association.

belongs_to :source_tweet, optional: true, inverse_of: :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
has_many :retweets, inverse_of: :source_tweet, class_name: 'Tweet', foreign_key: 'retweet_id'

With the above associations being set we can override the content getter and setter of the Tweet model.

def content
  if source_tweet
    source_tweet.content
  else
    super
  end
end

def content=(content)
  if source_tweet
    raise 'retweets cannot have content'
  else
    super
  end
end

# depending on preference the setter could also be written as validation
validates :content, absence: true, if: :source_tweet

Note that the above is not efficient when talking about query speed, but it's the easiest most clear solution. Solving parent/child queries is sufficiently difficult that it should get its own question, if speed becomes an issue.

If you are wondering why I set the inverse_of option. I would recommend you to check out the section Active Record Associations - 3.5 Bi-directional Associations.

Upvotes: 2

Hass
Hass

Reputation: 1636

Right now the error you're seeing is the one for strong params in Rails. If you can check your debugger or the HTTP post request that's being sent, you'd find that you don't have the params that you're "requiring" in retweet_params

def retweet_params
  params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
end

This is essentially saying that you expect a nested hash for the params like so

params = { retweet: { id: 1, content: 'Tweet' } }

This won't work since you're only sending the ID. How about something like this instead?

TweetsController.rb

class TweetsController < ApplicationController
  def retweet
    original_tweet = Tweet.find(params[:id])

    @retweet = Tweet.new(
      user_id: current_user.id,
      content: original_tweet.content
    )

    if @retweet.save
      redirect_to tweet_path, alert: 'Retweeted!'
    else
      redirect_to root_path, alert: 'Can not retweet'
    end
  end
end

Upvotes: 2

Related Questions