Tuck
Tuck

Reputation: 89

Limit number of Associations based on their value

I'm fairly new to this, so apologies in advance if this is a dumb question, or if I should be approaching this in a different way. I'm certainly open to almost all suggestions!

I'm working on creating a Jeopardy backend API using Rails 4. Currently, I've seeded my database with about 100 or so Categories, and each Category has multiple Clues, which house the question, answer and value. I also have a Game model which, when created, randomly selects up to 5 Categories and their clues.

My issue is that I want to know if there is a way to make sure that each Game only has five clues, and no duplicating values (ie, I don't want 5 clues that are all worth $100). I'm not sure if this is possible to do on the backend or if I should just filter it out on the frontend (I'm not using any Views - I'm building a separate dedicated JS client that will use AJAX to get the data from the API), but I feel like there has to be a Rails way to do this!

I'm not using any Join tables - Clues belongs_to Categories, and Categories has_many Clues, then a Game has_many Categories, so it's a pretty straight tree structure. I'm not sure what code would be helpful here for reference, since my Category and Clue models are pretty bare-bones at the moment, but here is what my Game model looks like:

class Game < ActiveRecord::Base
  after_create :assign_category_ids, :create_response, :reset_clues
  has_many :categories
  has_one :response, as: :user_input
  belongs_to :user

  # On creation of a new game, picks a user-specified 
  # number of random categories
  # and updates their game_ids to match this current game id
  def assign_category_ids
    game_id = id
    num_cats = num_categories.to_i
    @categories = Category.where(id: Category.pluck(:id).sample(num_cats))
    @categories.map { |cat| cat.game_id = game_id }
    @categories.each(&:save)
  end

  # Creates response to calculate user answer
  def create_response
    build_response(game_id: id, user_id: user_id)
  end

 # Resets categories and clues on every new game
  def reset_clues
    @categories = Category.where(game_id: id)
    @categories.each do |category|
      category.clues.each { |clue| clue.update_attributes(answered: false) }
      category.update_attributes(complete: false)
    end
  end
end

Any advice at all will be greatly appreciated!! Thank you in advance!

Upvotes: 1

Views: 54

Answers (2)

user229044
user229044

Reputation: 239311

I think there is some confusion in your underlying data model.

You're effectively mixing together two things that should be kept separate: The definition of categories and clues which you can think of as "static" data, and the game/response that users create, which is "dynamic" data.

A simplified (validation-free) implementation of your model layer should look something like this:

# This is pure data, it's the definition of a category
class Category
  has_many :clues
  # name: string
end

# This is pure data, it's the definition of a category, it's not tied to any user or game
class Clue
  belongs_to :category
  # answer: string
  # question: string
end

# This ties a specific user to a set of clues through GameClue
class Game
  belongs_to :user
  has_many :game_clues
end

# This ties together a Game, a Clue and the user's inputted answer
class GameClue
  belongs_to :game
  belongs_to :clue
  belongs_to :inputted_user_answer # Nil until a user inputs an answer
end

The key thing here is that Categories and Clues should never change. A Clue is the definition of a Clue, and many users may submit responses to it as part of many different games. The answer to this often-encountered problem is to create a completely separate type of record to hold user's responses, in this case GameClue: It joins together a Game, a Clue and a user's response, however you wind up recording that.

The idea here is that you can have as many Games as you want, with each Game sharing many of the same Clues and Categories, something that cannot be accomplished if a Game "takes ownership" of a specific Clue and uses that record to store the user's answer.


Regarding your original question about validation, and following along form the above data model, I would do something like the following untested code:

class Game

  def self.create_for(user)
    user.games.create do |game|
      [100,200,300,400,500].each do |points|
        Category.where(id: Category.pluck(:id).sample(5)).map do |cat|
          game.game_clues.create(clue: cat.clues.where(points: points).offset(rand(cat.clues.length)).first
        end
      end
    end
  end

  validates :game_clues, length: 5

  validate :must_have_clues_from_5_categories
  validate :must_have_proper_range_of_points

  protected

  def must_have_clues_from_5_categories
    if game_clues.pluck(:category_id).uniq.length < 5
      errors.add(:game_clues, :invalid)
    end
  end

  def must_have_proper_range_of_points
    if game_clues.pluck(:points).uniq.length < 5
      errors.add(:game_clues, :invalid)
    end
  end
end

The takeaway here is that you can use validate :method_name to supply a custom validation method that can check for complex conditions and add errors to the object, preventing it from being saved.

Upvotes: 1

Argonus
Argonus

Reputation: 1035

You can user has_many :clues through :categories and then validates :clues, length: { maximum: 5 }

Upvotes: 0

Related Questions