1

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)

2
  • Interresting question. Could you add the schema of your tables and your models as well? Commented Jul 25, 2017 at 4:33
  • Show us the Collection.rb model Commented Jul 25, 2017 at 5:02

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the details - You helped me understand and get further but I am still having a different issue, which led this to go slightly off topic. I have updated the question.
@DNorthrup, I've updated the answer, you're on the right track.
Thanks a lot for the additional details. I actually realized this after reading your response, and writing it down for review, I realized I should have a 'separate' model. I have since changed the data, and once I'm done with work I will see if I can successfully implement it and update.
Thanks a lot @Yaro - You helped me solve, incidentally, a bigger issue that was frustrating me. I will post my completed code above, and give you the answer. In my old set-up I was forced to submit the name+platform into EVERY row of 'Collection' which I didn't like at all.
Glad you got it sorted, and thanks for accepting the answer!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.