1

I have a Projects and a Relationships model to establish a 'following' relationship between a user and a project. I established three roles for the 'Relationship' using enum in the relationship model...they are admin, collaborator, and visitor. I need, however, to set the default role based upon the way in which the Relationship between user and project is established. The following simple scenarios are needed:

(a) A user who creates a project is automatically following the project...the Relationship role should be set to 'Admin' upon creation of the project

(b) if a visitor to the site simply navigates to a project profile page, they can click the 'Follow' button to establish a following relationship...however this should set the Relationship role to 'Visitor'

Relationship Model:

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "Project"
  validates :follower_id, presence: true
  validates :followed_id, presence: true

  enum role: [:admin, :collaborator, :visitor]
  after_initialize :set_default_role, :if => :new_record?

  def set_default_role
    self.role ||= :admin
  end
end

Relationships Controller:

class RelationshipsController < ApplicationController
  before_filter :authenticate_user!

  def create
    @project = Project.find(params[:relationship][:followed_id])
    current_user.follow!(@project)
    # relationship.visitor! View railsapps documentation for devise pundit#roles
    redirect_to @project
  end

  def destroy
    @project = Project.find(params[:id]).followed
    current_user.unfollow!(@project)
    redirect_to @project
  end

  private

  def relationship_params
    params.require(:relationship).permit(:followed_id, :follower_id)
  end

Projects Controller

class ProjectsController < ApplicationController
  before_filter :authenticate_user!, only: [:create, :new, :edit, :update, :delete, :followers]

def create
    @project = current_user.own_projects.build(project_params)
    if @project.save
      if params[:project][:projectimage].present?
        render :crop
      else
        flash[:success] = "You've successfully created a project..."
        redirect_to @project
      end
    else
      render 'new'
    end
  end

  def update
    @project = Project.find(params[:id])
    if @project.update_attributes(project_params)
      if params[:project][:projectimage].present?
        render :crop
      else
        flash[:success] = "Project Created"
        redirect_to @project
        @project.followers << current_user #this establishes the following relationship as soon as a new project is created between user/project
      end
    else
      render 'edit'
    end
  end

end

User Model:

class User < ActiveRecord::Base
  has_many :own_projects, :class_name=>'Project'

  has_many :projects
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy

  has_many :followed_projects, through: :relationships, source: :followed
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  def following?(some_project)
   relationships.find_by_followed_id(some_project.id)
  end

  def follow!(some_project)
   self.relationships.create!(followed_id: some_project.id)
   relationship.visitor!
  end

end

What would I need to change in my code in order to set the default role to either 'admin' or 'visitor' based on the two scenarios mentioned above?

BB500
  • 549
  • 2
  • 6
  • 24
  • 1
    Why don't you simply always specify the `role` when creating a new `Relationship`? Why bother with default value if it is different under different conditions (external to the relationship)? – Matouš Borák Mar 21 '16 at 23:22
  • if I removed the after_initialize and the set_default_role as you mention (and just left the - enum role: [:admin, :collaborator, :visitor]...how would I establish that relationship role type of 'admin' for a new project being created? In the create action of the projects controller or relationships controller? thx – BB500 Mar 21 '16 at 23:28
  • 1
    Yes, that is what I meant. You could set the relevant `role` essentially anywhere the relationship is created, e.g. in the `follow!` method in `User`: `self.relationships.create!(followed_id: some_project.id, role: :visitor)`, or in the controllers. – Matouš Borák Mar 21 '16 at 23:32
  • makes sense to me - but i removed as you mentioned and changed the follow! method in the User model as you specified (by just adding the role: :visitor), but its not setting the role. after creating the project, when i run the console and check the a.role it says => nil. Any idea as to why that might be? – BB500 Mar 21 '16 at 23:40
  • 1
    One reason may be that the underlying table column for `role` is not an integer. It must not be a string column but integer as the enum values are automatically mapped from symbols (strings) to integers. – Matouš Borák Mar 21 '16 at 23:52
  • hmmm - yea its definitely an integer. running console i get: d = y.relationships.last Relationship Load (0.3ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = $1 ORDER BY "relationships"."id" DESC LIMIT 1 [["follower_id", 3]] Relationship Load (0.3ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = $1 ORDER BY "relationships"."id" DESC LIMIT 1 [["follower_id", 3]] => # – BB500 Mar 21 '16 at 23:57
  • 1
    Hmm, I don't know here... I just tested my own simple enum and it works as I stated above. Can you add a similar `validates` `presence` to the relationship model for the `role` and see if a record creates then? – Matouš Borák Mar 22 '16 at 00:07
  • i added the validates :role, presence: true...and now it throws the following error when trying to create the project: ActiveRecord::RecordInvalid in ProjectsController#update Validation failed: Role can't be blank it points to the @project.followers << current_user as the problem line. really appreciate the help. – BB500 Mar 22 '16 at 00:14
  • 1
    Aha, I think I am beginning to see the problem that lead you to default value in the relationship model. There's actually a `has_many :through` relationship between `Project` and `User`! And AFAIK, you can't set other attributes by simply using `<<` to add to the collection. You'd have to create the relation manually, see e.g. [this post](http://stackoverflow.com/a/13015051/1544012) for a way how to do it. – Matouš Borák Mar 22 '16 at 00:23
  • ah i see...thats a very helpful link you found... man this is proving to be more complex than i thought hah - gonna take a breather. thanks for your help. – BB500 Mar 22 '16 at 00:41
  • so i removed the project.followers << current_user - and as per the blog post, replaced it with project.save && project.followers.create(user: :current_user, role: :collaborator). But now i get this error. ActiveRecord::UnknownAttributeError in ProjectsController#update unknown attribute 'user' for User. – BB500 Mar 22 '16 at 01:15

1 Answers1

1

As discussed in the above comments, I think that you should explicitly state the role when creating your Relationships, because the roles differ on conditions that are not inherent to the Relationship class.

Now, as the Relationship is the middleman in the has many :through association between your User and Project models, you cannot simply use the standard way of connecting users to projects using << but have to construct the middleman Relationship explicitly including your custom params (such as the role).

Yours solution should be analogical to the one posted here:

class Project < ActiveRecord::Base
  has_many :relationships, foreign_key: :followed_id, dependent: :destroy
  has_many :users, through: :relationships
end

class User < ActiveRecord::Base
  has_many :relationships, foreign_key: :follower_id, dependent: :destroy
  has_many :projects, through: :relationships

  # this will create the relationship association with the 'visitor' role
  def follow_project!(some_project)
    self.relationships.create!(followed_id: some_project.id, role: :visitor)
    # I think you can even omit the ids here and work with objects:
    # self.relationships.create!(followed: some_project, role: :visitor)
  end

  # this will create the relationship association with the 'admin' role
  def administrate_project!(some_project)
    self.relationships.create!(followed: some_project, role: :admin)
  end

  # this will create the relationship association with the 'collaborator' role
  def collaborate_on_project!(some_project)
    self.relationships.create!(followed: some_project, role: :collaborator)
  end
end

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "Project"

  enum role: [:admin, :collaborator, :visitor]
end

The follow_, administrate_ and collaborate_on_project! methods work the same way but each sets different role in the relationship. Then, you can simply call the appropriate one from the controller, for example to set the 'admin' relationship when creating a project:

class ProjectsController < ApplicationController

  def create        
    # ideally you should wrap multiple separate saves in a transaction so that
    # either they all succeed or all fail
    Project.transaction do
      # this creates a barebone project, without any association
      @project = Project.create!(project_params)
      # this associates the project to the current user with admin role
      current_user.administrate_project!(@project)
      # ...                 
    end
  end

end

Please be also sure to read rails guides on the :through associations carefully.

Community
  • 1
  • 1
Matouš Borák
  • 15,606
  • 1
  • 42
  • 53