Update attributes on has_many through associations and working with the unsaved object

2k Views Asked by At

This has something to do with my last quesion about unsaved objects, but now it is more about a specific problem how to use rails.

The models I have are:

class User < ActiveRecord::Base
    has_many :project_participations
    has_many :projects, through: :project_participations, inverse_of: :users
end

class ProjectParticipation < ActiveRecord::Base
    belongs_to :user
    belongs_to :project

    enum role: { member: 0, manager: 1 }
end

class Project < ActiveRecord::Base
    has_many :project_participations
    has_many :users, through: :project_participations, inverse_of: :projects

    accepts_nested_attributes_for :project_participations
end

With this models, when I create a new project I can do it by a form (fields_for etc) and then I can call update_attributes in the controller. So if I have users in the database already, I can do this:

u = Users.create # save one user in database (so we have at least one saved user)
p = Project.new

# add the user to the project as a manager
# the attributes could come from a form with `.fields_for :project_participations`
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> true

This works fine until I want to do something with the users of a project. For example I want add a validations that there must be at least one user for a project:

class Project < ActiveRecord::Base
    ...
    validates :users, presence: true # there must be at least one user in a project
    ...
end

This now gives:

u = Users.create
p = Project.new

p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> false

p.errors
=> #<ActiveModel::Errors:... @base=#<Project id: nil>, @messages={:users=>["can't be blank"]}>

p.users
=> #<ActiveRecord::Associations::CollectionProxy []>

p.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: 1, project_id: nil>]>

So on unsaved projects the .users is empty. This already bugs me (see my last quesion about unsaved objects). But in this case I can of course now work around this by doing validates :project_participations, presence: true instead of validates :users, presence: true and it should mean the same.

But this would mean I should never use the .users method (in any helper, model, view, ...) unless I am totally sure that I work with a saved object. Which in fact renders the .users method unusable (like it does with the validation of user`s presence).

If I call update_attributes like this, the validations works and it saves:

p.update_attributes(users: [u])

With this it creates the project_participation by itself so p.users works as expected. But here I cannot set any data like role for project_participation of that user.

So my questions are: Can I make the .users method work whether or not the object is saved (I think not)? But then, how can I add users to a unsaved project as a manager/member and work with the unsaved project?

I hope my problem is clear.

1

There are 1 best solutions below

2
On

I think I understand you question, and you're correct in assuming that you cannot use the .users method whether or not the project model is saved. The reason for this is that in defining an association in Project (ie. has_many :users, through: :project_participations, inverse_of: :projects) you're telling rails to read the users attribute out of the database via the project_participations join table and when you haven't saved the project you have nothing to read out of the database.

In order to add a User to your project in a particular role you will need to create a new ProjectParticipation model which you will then associate to your project. If you then remove the users association and write your own users method you should be able to access your collection of users regardless of whether or not the project has been saved.

class Project < ActiveRecord::Base
    has_many :project_participations

    ...

    def users
        project_participations.collect { |pp| pp.user }
    end
end

Then something like:

u = Users.create
p = Project.new

pp = ProjectParticipation.new({user: u, project: p, role: 1})
p.project_participations << pp
p.users

Hopefully that helps.