Reputation: 106
Imagine you were building a pretty complex application for professors to give exams to their students. You might imagine the rails model layer might look like:
class Exam < ActiveRecord::Base
belongs_to :professor
has_many :questions
has_many :student_exams
# exam_name: string
# exam_start_time: string
end
class StudentExam < ActiveRecord::Base
belongs_to :student
belongs_to :exam
end
class Question < ActiveRecord::Base
belongs_to :exam
has_many :answers
# question_text: text
end
class Answer < ActiveRecord::Base
belongs_to :student_exam
belongs_to :question
has_many :gradings
# answer_text: text
end
class Grading < ActiveRecord::Base
belongs_to :answer
belongs_to :grader
# score: integer
# grader_notes: text
end
Ignore, at least for the point of this question, any other models that could exist (Why isn't there a Course model that belongs_to :professor and then has_many :students? What does Professor and Student model look like?).
The idea is that a Professor
creates an Exam
with Questions
, associates Students
with an Exam
(via StudentExam
), and those Students
then create a unique Answer
to each Question
. Each Answer
then gets a Grading
by one or more Graders
.
The problem comes when I'm trying to make the grading form -- what would the controller update
and create
look like, along with the form? As a Grader, you would want the entire exam laid out, where you could then evaluate a student's answers. So I was thinking a form might look like:
= simple_form_for @exam do |f|
= f.object.exam_name
= f.simple_fields_for :questions do |q|
= q.object.question_text
= q.simple_fields_for :answers do |a|
= a.object.response_text
= a.simple_fields_for :gradings do |g|
= g.score
= g.grader_notes
(This assumes, of course, that Exam
accepts_nested_attributes_for :questions
, Question
accepts_nested_attributes_for :answers
, Answer
accepts_nested_attributes_for :evaluations
).
The problem, in my mind, with this form is that I feel it is pretty deeply nested and hard to validate. Thoughts that come to mind:
q.simple_fields_for :answers
isn't this just scoped to just the Student
I'm grading's Answers
, and isn't really linked back to the Exam
, right?a.simple_fields_for :gradings
isn't this scoped to not only my gradings
as a Grader
, but other Graders
as well, right?On the controller side when I submit, how can I validate that as a Grader
, I'm not modifying another Grader's
Grading
? Or that a Grader
isn't maliciously associating a Grading
with a different Answer
that's part of a different Exam
they might not have access to? What would the create/update look like? What would the strong_parameters look like? My first guess would be something like:
class GradingController < ActionController::Base
# ....
create
@exam = grader.exams.find(params[:id])
@exam.new(grading_params)
end
private
def grading_params
params(:exam).permit(:questions_attributes => [:answers_attributes => [:grading_attributes => [:score, :grader_notes]])
end
end
But now how do I associate the Grader
with the Gradings
? I didn't add :grader_id
to the strong_params for grading_attributes, because then they could submit up parameters with a different grader_id
than their own.
Similarly, I didn't put the :id
column in the answers_attributes, because I feel someone could then maliciously change that ID and associate one of their Gradings
with a different Answer
on a different Exam
, especially because an Answer
is only tied to a Question
with a foreign_key, and not back to an Exam
(right?).
The only way I can think to solve this then would be to break apart the parameters submitted and check them all along the way, which seems crazy. IE something like:
class GradingController < ActionController::Base
# ....
def create
@exam = grader.exams.find(params[:id])
params[:exam][:question_attributes].each do |question_hash|
question = @exam.questions.find(question_hash[:id])
if question.present?
answer_hashes = question_hash[:answer_attributes]
answer_hashes.each do |answer_hash|
answer = question.answers.find(answer_hash[:id])
if answer.present?
grading_hashes = answer_hash[:grading_attributes]
grading_hashes.each do |grading_hash|
grading = answer.gradings.where(grading_hash[:id]).first_or_initialize
grading.grader = current_grader
grading.score = grading_hash[:score]
grading.grader_notes = grading_hash[:grader_notes]
end
end
end
end
end
end
end
This seems very brittle, and also incredibly complex, along with impossible to return from correctly (How do we handle or display errors back on the form if Answer or Question isn't present, etc). But what's the other option here?
Upvotes: 4
Views: 162
Reputation: 3437
First off, the approach you described above if not very good for complex froms as in your case, as you already found out yourself. Using accepts_nested_attributes and nesting all fields inside other fields is getting complex very fast, and has a very static nature in my oppinion (and its also hell to maintain).
What you can do in cases, that are too complex for the simple accepts_nested_attirbute functions of rails is use form objects to handle validation and saving of multiple models. This approach does not need this nesting of models and pays off the more complex your form gets. As a first reference of what form objects are you can read in THIS very nice post (section 3).
Basically, what you do is you move some, or if you create a form classes, that can store all information a form picks up and save all models, once the validation succeeds. In contrast to the above metioned blog post, that uses the virtus gem for creating virtual fields, I personally use ActiveData for this job, but you can also stick with your ActiveRecord models and keep the validation there and forward the errors to your form object.
What your form object should do is:
You can get this to work with simple_forms, by manually setting the parameters to a structure, that you think makes sense, while your form object can work with those structures.
I think there is no general recepie for creating your form object but I think with the above information you should have a good start into figuring out the best way to create your form object.
As an example I can show you a form object, of a shopping cart that I created, which works without an extra gem and also works with multiple steps, by saving data to the session: Form Object, Step 1, Step 2, Step 3
Hope this helps.
Upvotes: 1