has many through with an extra field

Hello,

I'm trying to solve a common task with many to many relationships.

Here are my class definitions:

class User < ActiveRecord::Base
  has_many :questionnaires
  has_many :forms, :through => :questionnaires
end

class Form < ActiveRecord::Base
  has_many :questionnaires
  has_many :users, :through => :questionnaires
end

class Questionnaire < ActiveRecord::Base
  belongs_to :user
  belongs_to :form
end

My problem is to include an extra date field named active_at to User
new/edit form.

When creating a user I need to check forms (via checkboxes) which it
belongs to. I know it can be done by creating checkboxes with name like
'user[form_ids][]'. But I also need to enter a date field for each of
checked relations which will be stored in the questionnaire join model.

Any help is very appreciated
Bodo

Hello,

I’m trying to solve a common task with many to many relationships.

Here are my class definitions:

class User < ActiveRecord::Base

has_many :questionnaires

has_many :forms, :through => :questionnaires

end

class Form < ActiveRecord::Base

has_many :questionnaires

has_many :users, :through => :questionnaires

end

class Questionnaire < ActiveRecord::Base

belongs_to :user

belongs_to :form

end

My problem is to include an extra date field named active_at to User

new/edit form.

When creating a user I need to check forms (via checkboxes) which it

belongs to. I know it can be done by creating checkboxes with name like

‘user[form_ids][]’. But I also need to enter a date field for each of

checked relations which will be stored in the questionnaire join model.

Any help is very appreciated

We recently had a long (and confusing) discussion about this

https://groups.google.com/forum/#!topic/rubyonrails-talk/W-FTZNPNUeE/discussion

Looking back at it, I would suggest take it step-by-step.

Try first to understand how to get it to work on the model level, write unit tests for
that and only after that, build your new/edit view code that will generate the params

hash that will fill in the values.

There is a good chance you will need to do some “manual” tweaking to
completely build up the datastructure from the params hash. I mean a
simple user = create(params[:user]) may require you defining some

additional setter methods.

Maybe, the core is that you will need to override the

form_ids=(id,id,...)

method on the User model (start with

rails c

User.new.methods.grep(/form_ids=/)

to see if it is defined.

Assuming you use Rails 3.2.x you could start playing with

(UNTESTED code, probably not optimal, just a hint):

class Questionnaire < ActiveRecord::Base

belongs_to :user, :inverse_of => :users

belongs_to :form, :inverse_of => :forms # important for the save after build !

google this line: "The last line ought to save the through record (a Taggable).

This will only work if the :inverse_of is set:"

end

class User < ActiveRecord::Base
has_many :questionnaires
has_many :forms, :through => :questionnaires

def form_ids=(form_id_array)
super # will pass the argument to higher-up function and build the associated forms

self.forms.each do |form|
  # I presume this will be populated by now
  qs = form.questionnaires
  qs = qs.select{|q| q.user == self} # filter only those that are this user
  raise "BOOOM" if qs.size > 1  # there can be only 1 (check it to be sure)

  q = qs.first
  q.active_at Time.now
end

end

end

Then check the result manually and with tests.

Then save the user and check if all is still correct.

Once you can set the active_at to Time.now, a next phase can start to
set it to actual values, that are gotten from the form (probably need

to make a non-standard input format that may be an array of hashes
with in each entry the form_id and the date for that form_id ??).

I hope this helps, but I keep finding this non-trivial …

If I overlook the obvious, standard solution, I would be glad to be corrected.

Peter

Thanks.

I forgot to mention that one solution is discussed on

http://stackoverflow.com/questions/9174513/rails-has-many-through-form-with-checkboxes-and-extra-field-in-the-join-model/9191867#9191867

When I try this approach I got an error message from inside the original
routine below:

Couldn't find Questionnaire without an ID

(In my case Product is User, Category is Form and Categorization is
Questionnaire.)

def initialized_categorizations # this is the key method
    [].tap do |o|
      Category.all.each do |category|
        if c = categorizations.find { |c| c.category_id == category.id }
          o << c.tap { |c| c.enable ||= true }
        else
          o << Categorization.new(category: category)
        end
      end
    end
  end
...

My question is what the error line tries to do in detail.

Anyway, I'm surprised that for this common problem there is still no
reasonable solution available.

Regards
Bodo

In that case, could you then send your particular implementation
and the stack trace that you got.

A wild guess is that there is a problem where looking for

an ID (that is only generated when saving to the database;
but at this time your questionaire object is still in the making
in memory and does not have an id yet).

That is why I used the “select” code in my proposal (checks

in the array in memory using the belongs_to result == self)
and not find (which would look for an entry in db, but we have
not saved yet at that point).

HTH,

Peter