I am building a question engine with Ruby, I have the question element working well - users can create and attempt questions
, and their attempts are saved in a question_attempts
table.
I am now trying to build a question bank feature where users can filter all questions by a few different options - tags, difficulty and the result of their last attempt (if it exists). Both the filtering by tag (via a separate tags
table and join taggings
table), and difficulty (a string of four options saved within the question
table) work as expected.
I am having some issues with filter questions based upon the user's attempts. I need to be able to filter for three different conditions:
- Correct - where question.attempt.score == 100
- Incorrect - where question.attempt.score < 100
- Not attempted - where question.attempt == nil / does not exist
The question_attempts
models belongs_to: user
and belongs_to: question
. The score
from their attempt is saved as a standardised percentage float with a maximum of 100.0.
For the first two I can use a similar methodology as I have for the tags, both of these work:
.where(question_attempts: { score: 100, user_id: current_user.id }
.where.not(question_attempts: { score: 100 }).where(user_id: current_user.id })
I can get the questions that an individual user has not submitted a response to using the following line:
.where.not(id: QuestionAttempt.where( user_id: current_user.id ).select( "question_id" ) )
My issues are:
- How do I combine these filter options? I am trying to avoid writing a long series of
if else
statements, but will do this if required (I can do this without assistance, just want to check there isn't a better way before cracking on) - How do I limit the checking of results to their last attempt only? i.e. if they have submitted a question response 2+ times I only want to check the result of the most recent. I would be tempted to use the solution from Mark Swardstrom in this answer, primarily because I understand it, but am concerned it would be an inefficient method.
Thank you for your help
Let me know if you need more details.
Models
class Question < ApplicationRecord
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
has_many :question_attempts, dependent: :destroy
end
class QuestionAttempt < ApplicationRecord
belongs_to :question
belongs_to :user
end
Controller
# Gather topics / tags
# This is working
if params[:topics] != nil
@tags = params[:topics].split(",")
end
# Gather difficulties
# This is working as expected
# If nil, then show all difficulties
if params[:difficulties] != nil
@difficulties = params[:difficulties].split(",")
else
flash[:notice] = 'Showing all difficulties as none selected'
@difficulties = ["Untested", "Easy", "Medium", "Hard"]
end
# This is the element not yet working
# I can gather the options Unattempted / Correct / Incorrect from the params
# Not sure how to then join this with my question_attempts table
@attempts = params[:attempts].split(",")
# Gather questions matching the filters
if @tags == nil
@questions = Question.joins(:question_attempts).where(difficulty: @difficulties, private: false, draft: false, approved: true).distinct
else
@questions = Question.joins(:question_attempts).joins(:tags).where(tags: { name: @tags }, difficulty: @difficulties, private: false, draft: false, approved: true).distinct
end
Current Solution
This is likely not the most efficient way of doing this, but it seems to be working thanks to tips from Tom Lord. Would welcome any constructive feedback / tips.
def tags
params[:topics]&.split(",")
end
def difficulties
params[:difficulties]&.split(",")
end
def filter
# Check for previous answers
if params[:correct] == "true" && params[:incorrect] != "true"
@questions = Question.where( id: QuestionAttempt.where(user_id: current_user.id).where('score = 100').pluck(:question_id) )
elsif params[:incorrect] == "true" && params[:correct] != "true"
@questions = Question.where( id: QuestionAttempt.where(user_id: current_user.id).where('score < 100').pluck(:question_id) )
elsif params[:incorrect] == "true" && params[:correct] == "true"
@questions = Question.where( id: QuestionAttempt.where(user_id: current_user.id).pluck(:question_id) )
end
# Check for unanswered filters
if params[:unattempted] == "true"
if @questions == nil
@questions = Question.where.not( id: QuestionAttempt.where( user_id: current_user.id ).pluck(:question_id) )
else
@questions = Question.where.not( id: QuestionAttempt.where( user_id: current_user.id ).pluck(:question_id) ).or (
@questions
)
end
end
# Add all other filters afterwards as they should apply to all
@questions = @questions.where(difficulty: difficulties) if difficulties
@questions = @questions.joins(:tags).where(tags: { name: tags }) if tags
@questions = @questions.where(
private: false,
draft: false,
approved: true
)
@questions.distinct