DNorthrup
DNorthrup

Reputation: 847

Rails Datatables, similar tables in two controllers

I am using the JQuery Datatables gem and I have successfully gotten it to work for one of my Controllers. SteamGames.

SteamGames Controller

def index
  @steam_games = SteamGame.all
  respond_to do |format|
    format.html
    format.json { render json: SteamGamesDatatable.new(view_context) }
  end
end

The Datatable itself is pretty simple, from what I got from the git.

class SteamGamesDatatable
  delegate :params, :link_to, :number_to_currency, to: :@view

  def initialize(view)
    @view = view
  end

  def as_json(options = {})
    {
      sEcho: params[:sEcho].to_i,
      iTotalRecords: SteamGame.count,
      iTotalDisplayRecords: games.total_entries,
      aaData: data
    }
  end

private

  def data
    games.map do |game|
      [
        link_to(game.game_name, game),
        "<img src='#{game.img_icon_url}'/>",
        game.created_at.strftime("%B %e, %Y"),
        game.updated_at.strftime("%B %e, %Y"),
      ]
    end
  end

  def games
    @games ||= fetch_games
  end

  def fetch_games
    games = SteamGame.order("#{sort_column} #{sort_direction}")
    games = games.page(page).per_page(per_page)
    if params[:sSearch].present?
      games = games.where("game_name like :search", search: "%#{params[:sSearch]}%")
    end
    games
  end

  def page
    params[:iDisplayStart].to_i/per_page + 1
  end

  def per_page
    params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
  end

  def sort_column
    columns = %w[game_name img_icon_url created_at]
    columns[params[:iSortCol_0].to_i]
  end

  def sort_direction
    params[:sSortDir_0] == "desc" ? "desc" : "asc"
  end
end

Now I'm setting it up for another controller Collections but I realize I'm being extremely redundant but I don't know how to resolve it.

"Collections" is the middleware link between Users and SteamGames.

So I thought perhaps I could just duplicate the ENTIRE Datatables code and replace SteamGame with Collection.steam_game as I would in the Rails Console, but it informs me

NoMethodError (undefined method 'steam_game' for #<Class:0x00000007159670>):

The purpose of this is if I go to /collection/:id I will only see games THAT collection owns. /steamgames shows me every game in my database.

How could I leverage the previous logic easily within the new controller?

If I can't, then how do I properly reference a relational link within a controller?

FYI This is the Datatable I tried making for Collections out of curiousity

class CollectionsDatatable
  delegate :params, :link_to, :number_to_currency, to: :@view

  def initialize(view)
    @view = view
  end

  def as_json(options = {})
    {
      sEcho: params[:sEcho].to_i,
      iTotalRecords: Collection.count,
      iTotalDisplayRecords: games.total_entries,
      aaData: data
    }
  end

private

  def data
    games.map do |game|
      [
        link_to(game.game_name, game),
        "<img src='#{game.img_icon_url}'/>",
        game.created_at.strftime("%B %e, %Y"),
        game.updated_at.strftime("%B %e, %Y"),
      ]
    end
  end

  def games
    @games ||= fetch_games
  end

  def fetch_games
    games = Collection.steam_game.order("#{sort_column} #{sort_direction}") ##<--- This is where the error comes from
    games = games.page(page).per_page(per_page)
    if params[:sSearch].present?
      games = games.where("game_name like :search", search: "%#{params[:sSearch]}%")
    end
    games
  end

  def page
    params[:iDisplayStart].to_i/per_page + 1
  end

  def per_page
    params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
  end

  def sort_column
    columns = %w[game_name img_icon_url created_at]
    columns[params[:iSortCol_0].to_i]
  end

  def sort_direction
    params[:sSortDir_0] == "desc" ? "desc" : "asc"
  end
end

I was thinking maybe an additional function within SteamGamesController might suffice, so I can over-write the def fetch_games function but I don't fully understand what SteamGamesDatatable.new(view_context) is calling within the controller. I ~assume~ the initialize(view) function?

Collection Model

class Collection < ApplicationRecord
    belongs_to :user
    belongs_to :steam_game
end

SteamGames is actually very similar

Schema for Collection/SteamGames

create_table "collections", force: :cascade do |t|
    t.string "platform"
    t.string "name"
    t.bigint "user_id"
    t.bigint "steam_game_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["steam_game_id"], name: "index_collections_on_steam_game_id"
    t.index ["user_id"], name: "index_collections_on_user_id"
  end

  create_table "steam_games", force: :cascade do |t|
    t.integer "appid", null: false
    t.string "game_name", default: "", null: false
    t.string "img_icon_url", default: "assets/32x32-no.png"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

Update 2 - Passing Additional Initialization

class CollectionsDatatable
  delegate :params, :link_to, :number_to_currency, to: :@view

  def initialize(view, steam_games_resource)
    @view = view
    @steam_games_resource = steam_games_resource
  end

  def as_json(options = {})
    {
      sEcho: params[:sEcho].to_i,
      iTotalRecords: @steam_games_resource.count,
      iTotalDisplayRecords: games.total_entries,
      aaData: data
    }
  end

private

  def data
    games.map do |game|
      [
        link_to(game.game_name, game),
        "<img src='#{game.img_icon_url}'/>",
        game.created_at.strftime("%B %e, %Y"),
        game.updated_at.strftime("%B %e, %Y"),
      ]
    end
  end

  def games
    @games ||= fetch_games
  end

  def fetch_games
    games = @steam_games_resource.order("#{sort_column} #{sort_direction}")
    games = games.page(page).per_page(per_page)
    if params[:sSearch].present?
      games = games.where("game_name like :search", search: "%#{params[:sSearch]}%")
    end
    games
  end

  def page
    params[:iDisplayStart].to_i/per_page + 1
  end

  def per_page
    params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
  end

  def sort_column
    columns = %w[game_name img_icon_url created_at]
    columns[params[:iSortCol_0].to_i]
  end

  def sort_direction
    params[:sSortDir_0] == "desc" ? "desc" : "asc"
  end
end

Controller

  def show
    @collection = Collection.find(params[:id])
    respond_to do |format|
      format.html
      format.json { render json: CollectionsDatatable.new(view_context, @collection.steam_game) }
    end
  end

I have modified the initialization to accept a new parameter, which I might be complicating. I also went through the datatable to remove the instance of Collection.steam_game.

Presently I am getting a undefined methodcount' for #` response, which makes me believe that it is trying to .count on a singular game. I think this is because every record is inserted into the Collection table - So even though it outputs a 'steam_game', there is no count.

After getting this far, I think my models might not be set up properly.

A Member should have a "Collection" - Collection has a platform and a name. The Collection should have games. In theory this is proper, but I'm noticing every game is creating a new Collections row.

Should I instead have a User Collection GameCollections Game

system where the GameCollection is nothing but the 'union'? And User has Collections?

Final Update

Thanks to @Yaro's answer below, it helped guide me on the proper solution.

I went with a 4 Step Sync. User -> Collection <-> Game Collections <- Steam_Games

This allows me to find all users who have X steam_game and find all Collections that have X steam_game

After fixing the logic, I was able to use the same Datatable with the provided recommendation.

My CollectionsController

  def show
    @collection = Collection.find(params[:id])
    respond_to do |format|
      format.html
      format.json { render json: SteamGamesDatatable.new(view_context, @collection.steam_games) }
    end
  end

This now shows only the games applicable to this specific collection. I now need to re-visit the naming convention, but this is exactly what I needed.

(Side-note, it also worked with created an exact duplicate CollectionsDatatable but that felt very repetitive)

Upvotes: 1

Views: 359

Answers (1)

Yaro Holodiuk
Yaro Holodiuk

Reputation: 588

I don't fully understand what SteamGamesDatatable.new(view_context) is calling within the controller. I ~assume~ the initialize(view) function?

You are right, #new does call the #initialize method of SteamGamesDatatable. You can add any logic to your #initialize as long as you don't overwrite the @view assignment.

I think the notable issue here is that you are trying to call Collection.steam_game - you get the NoMethodError because the Collection class really does not know where to look for this method. I suppose what you're looking for is collection.steam_games. There's two parts to this - first, you need an instance of Collection to represent the concrete record, while now you supply the class which represents the table in general. Second, I suspect, in your Collection model you have has_and_belongs_to_many :steam_games, so there's no singular form (steam_game) your model will be able to look up.

As to the essence of your question - how to re-use your code, here's what I would do: I'd only leave the class SteamGamesDatatable and on its instantiation would add another argument:

def initialize(view, games_resource)
  @view = view
  @games_resource = games_resource
end

The games_resource for your Steam Game Controller #index action would be all Steam Games (SteamGame.all), and for your Collection Controller it would be the steam games of that concrete collection (e.g. @collection.steam_games). Then, in your fetch_games you'd use the games_resource instead of concrete model to fetch the games.

If you follow this path and find yourself in a situation where you need some separate datatable logic for your collections, you can always move all code, that is duplicated, to a module and include that module in any number of classes you'd want to share the logic across.

Hope this helps you.

Update

Should I instead have a User Collection GameCollections Game

system where the GameCollection is nothing but the 'union'? And User has Collections?

Yes, sorry, I just guessed your database tables structure, which might've led to some redundant actions.

But essentially yes, if your Collection belongs_to one steam_game, you will not be able to output 'all' steam games of a collection, because there is one game per one collection. So your approach is correct - just create a join table between collections and steam games.

So in the end you will have these relationships:

  • User has_many :collections
  • GameCollection belongs_to :collection; belongs_to :steam_game
  • Collection belongs_to :user; has_many :game_collections; has_many :steam_games, through: :game_collections
  • SteamGame has_many :game_collections; has_many :collections, through: :game_collections

The relation between Collection and SteamGame would look a bit neater with has_and_belongs_to_many, but this is frowned upon by style guides.

Upvotes: 1

Related Questions