Topic: Best approach for types of users?

I was wondering what route I should go for this.

Let's say there are voters, filmmakers, judges, and an admin.

What would be the best modeling relationship for this setup?

Obviously they share one common thing, they are all a type of user. So should it be STI, or a polymoprhic type of association? And could you also provide an example to help picture it?

Also, when registering they should be able to specify what type of user they are registering as. I suppose this can be done using a paramter?

I'd appreciate it - thanks!

edit: I was thinking something like: users, user_types, and installing http://agilewebdevelopment.com/plugins/acl_system to specify what type of user has access to what actions, etc. Well also, user_types need to have specific features, and the original way I was thinking was just an id, and title. Arg I'm a bit confused.

Would this approach be viable?

edit:  Well also, user_types need to have specific features, and the original way I was thinking was just an id, and title. Arg I'm a bit confused. Like I'd rather seperate the logic from only using the users table.

Basically the source of confusion is that, let's say a voter is only able to have a blog, nobody else.

Last edited by DFischer (2007-04-30 22:48:39)

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

STI is a great way to go, I have used it and it has worked well. The reason is that it's very easy to override the behavior for the different types of users using inheritence. For example:

class User < ActiveRecord::Base
  def can_have_blog?
    false
  end
end

class Voter < ActiveRecord::Base
  def can_have_blog?
    true
  end
end


Then it's just a matter of asking the current user if he can have a blog. All the permission code is contained within the user model and subclasses.

Letting the user select the type while registering can be a little tricky because "type" is protected from mass assignment so you have to set it directly. If you have trouble, feel free to post and I can explain more.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

So basically on signup pull the paramter they choose and make itself.type the parameter?

user = New.etc....
user.type params[:type]
user.save

yea?

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

I think you have to set it like this:

user[:type] = params[:type]

Because "type" is also a reserved method name in Ruby so you can't really set it directly. No clue why they gave it that name.

You may want to create a virtual attribute for setting type, but just call it "kind".

# in User model
def kind
  read_attribute(:type)
end

def kind=(kind)
  write_attribute(:type, kind)
end


That way you can just use "kind" instead of "type" in the controller/view so you don't have to manually set it in the controller. Untested.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

I have a question regarding this setup, so let's say there are questions and answers.

A voter will come along and want to ask a filmmaker a question, how would I plug this type of functionality in?

So far I figured it would be a HABTM type of relationship for questions and answers

questions --> answers_questions <-- answers

But how would the model go in? user has_many questions, user has_many answers or will that happen into the model that makes the most sense? voter has_many questions filmmaker has_many answers ?


ryanb wrote:

STI is a great way to go, I have used it and it has worked well. The reason is that it's very easy to override the behavior for the different types of users using inheritence. For example:

class User < ActiveRecord::Base
  def can_have_blog?
    false
  end
end

class Voter < ActiveRecord::Base
  def can_have_blog?
    true
  end
end


Then it's just a matter of asking the current user if he can have a blog. All the permission code is contained within the user model and subclasses.

Letting the user select the type while registering can be a little tricky because "type" is protected from mass assignment so you have to set it directly. If you have trouble, feel free to post and I can explain more.

Shouldn't voter be inheriting user?

Last edited by DFischer (2007-05-04 20:29:51)

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

DFischer wrote:

So far I figured it would be a HABTM type of relationship for questions and answers

questions --> answers_questions <-- answers

So an answer can answer many questions? Perhaps what you want is a one-to-many association where answer belongs_to question? On second thought, how different is the Answer model from the Question model? Aren't they both simple message content? Perhaps you should just make one "Message" model and make it acts_as_tree with a parent_id column. This way you can record answers/responses as a child of the original question.


DFischer wrote:

But how would the model go in? user has_many questions, user has_many answers or will that happen into the model that makes the most sense? voter has_many questions filmmaker has_many answers ?

You may want to put the association in the User model if it applies to all users. If you go with the Message approach you would have a given message belongs_to User and User has_many :messages.

DFischer wrote:

Shouldn't voter be inheriting user?

Oops, yep it should.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

ryanb wrote:
DFischer wrote:

So far I figured it would be a HABTM type of relationship for questions and answers

questions --> answers_questions <-- answers

So an answer can answer many questions? Perhaps what you want is a one-to-many association where answer belongs_to question? On second thought, how different is the Answer model from the Question model? Aren't they both simple message content? Perhaps you should just make one "Message" model and make it acts_as_tree with a parent_id column. This way you can record answers/responses as a child of the original question.


DFischer wrote:

But how would the model go in? user has_many questions, user has_many answers or will that happen into the model that makes the most sense? voter has_many questions filmmaker has_many answers ?

You may want to put the association in the User model if it applies to all users. If you go with the Message approach you would have a given message belongs_to User and User has_many :messages.

DFischer wrote:

Shouldn't voter be inheriting user?

Oops, yep it should.

Interesting on what you said regarding the acts_as_tree functionality. I think that would definitely do, I've never used acts_as_tree before so I'm not exactly sure how to go about it, but I think it should be fairly easy. no crazy hoops right?

If I put it in the user model, then I will take use of that constraint logic you mentioned earlier right?

##user model
def user_can_ask_question
   false
end

def user_can_respond_to_question
   false
end

##voter model
def user_can_ask_question
    true
end

##filmmaker model
def user_can_answer_question
    true
end


This is the approach I should take, yes?

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

DFischer wrote:

Interesting on what you said regarding the acts_as_tree functionality. I think that would definitely do, I've never used acts_as_tree before so I'm not exactly sure how to go about it, but I think it should be fairly easy. no crazy hoops right?

Right, it's really easy. Just add an integer parent_id column to the table and add acts_as_tree to the model. Then you get a lot of fun methods such as parent and children. See the docs for details.

DFischer wrote:

If I put it in the user model, then I will take use of that constraint logic you mentioned earlier right?

Yeah, that should work. Somewhere in the app you'll have to distinguish between a question and an answer since it uses the same model. I guess a question would be a Message with parent_id set to nil, and an answer will be a Message with parent_id set to some other message id.

Alternatively you can add a "can_create_message?" method to the user model and subclasses which can check if the parent_id is set and return true/false. It's basically the same thing.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

So before I implement this, my brain said "wait, how would this belong to multiple users?"

Point being, if a voter asks a film-maker a question, then the question belongs to a voter. But as an answer to that question, the answer should belong to the film-maker.

So, as functionality in the model I'd have to create something so that if the message is an answer, the user_id is set to the film-maker, and the same for question being owned by a voter.

Well.. I suppose that if only a film-maker can answer a question then all I need to do is just imprint that user_id on a record eh? Just thinking out loud I suppose, I'll come back if I run into a problem.

edit: Wait, I just realized something. Why are we going through trying to figure out the logic through a model that shares similarities, why not just make it work through STI, unless acts_as_tree doesn't have support for such a thing?

edit x2: more complications, how would this be directed towards a specific film-maker? (like a film maker can view all his questions, and then respond to them as answers).

Last edited by DFischer (2007-05-07 20:10:40)

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

Whenever I have these kinds of questions there is always one solution: go back to the interface. Sounds like you need to nail down some areas of the interface before continuing. How do you expect the interface to look for creating a question and an answer? Does it look the same? How do you plan for the film-maker to respond to already given questions? Once you get the interface down I think the implementation will be more clear.

Also, I find it helps to forget about permissions and access restrictions at the beginning. Don't worry about who can ask and answer questions, just build the question asking and answering functionality in, then add the permissions and restrictions afterwards. This way you have a lot of flexibility in the permission handling. This has worked really well for me, but results may vary. It really depends on how tightly the interface should depend on the permissions/roles.

Hope I'm not confusing you. smile

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

Well it seems like there has to be two references to a user_id

owner_id and the receiving_id

I think that can simply be added on into the db by calling the fields on the controller and then imprinting the value.

I suppose it's like an inbox/message type of thing - the best example I can give is how ebay handles questions. You ask a buyer a question, and the buyer can respond to it with a message back to the askee, he also has the option of making the question/answer public on his page for others to view.

That's what I'm trying to do.

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

Makes sense. You could do this in the model:

class Message < ActiveRecord::Base
  belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
  belongs_to :recipient, :class_name => 'User', :foreign_key => 'recipient_id
  acts_as_tree
end

When creating a message you just have a form setting the content and recipient_id. The author_id can automatically be set to the current user.

When replying to a message you can set both the recipient_id and owner_id column automatically. You would also need to set the parent_id to the id of the original message. Sounds like it will work pretty well. If you need the "make public" option you could do this with a simple boolean column.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

Arg, sorry to bump an old thread, but how in the heck would I do this in a restful approach?

I'm trying to figure out the index, that's easy:

  def index
    if params[:user_id]
      @user = User.find(params[:user_id])
      @messages = @user.messages
    end

    respond_to do |format|
      format.html # index.rhtml
      format.xml  { render :xml => @messages.to_xml }
    end
  end


Creating one for a user is fairly easy as well, just pass in a user_id to new (/new?user_id=1) then filling in the to field with @user.id

  def new
    @user = User.find_by_id(params[:user_id])
    @message = Message.new
  end

But replying? I think I could differentiate a create by a param that indicates whether it is a reply or not, but still "clouded".

Any input on this?

Also, would this be correct in my view for a proper recipient? I wasn't sure whether to set by id or recipient, or what the value should be. If it's by id, how would I make the value in the field be the login, but in the html be the id? (be ready for haml haiku)

%h1 New Message

= error_messages_for :message

- form_for(:message, :url => messages_path) do |f|
  %p
    %label To
    = f.text_field :recipient, :value => @user.login
  %p
    %label Question?
    = f.text_area :body
  %p
    = submit_tag "Create"


I really wish I could spec this out in RSpec but apparently I'm horrible at controller speccing, stubbing, modeling, lol.

Forgive me for being so tired so I may sound vague or bad structured, but I understand how the system works, it's just the controller part that is giving me head aches, especially this late.

(proof of model spec to show relationship functionality, and state of not being a total noob: http://pastie.caboo.se/67179)

btw: I think this would be a great screencast, an episode on how to create virtual messaging functionality.

Last edited by DFischer (2007-06-02 05:34:33)

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

DFischer wrote:

But replying? I think I could differentiate a create by a param that indicates whether it is a reply or not, but still "clouded".

How about passing the parent_id value in the params? If it has a parent_id then it's a reply.

DFischer wrote:

Also, would this be correct in my view for a proper recipient? I wasn't sure whether to set by id or recipient, or what the value should be. If it's by id, how would I make the value in the field be the login, but in the html be the id? (be ready for haml haiku)

Are you sure you want a text field to represent who the message is to? How about a select menu (if you don't have many users)? This way you can store the user's id as the value of each option.

If you really want a text field, look into creating a virtual attribute. It could look something like this:

# in message model
def recipient_login
  @recipient_login || (recipient && recipient.login)
end

def recipient_login=(login)
  @recipient_login = login
  self.recipient = User.find_by_login(login)
end

def validate
  errors.add(:recipient_login, "was not found") if recipient.nil?
end


Then you would use "recipient_login" as the attribute in the text field and you don't have to worry about setting the value or anything.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

ryanb, two questions:

1. What does STI stand for? Google didnt help on this.

2. In the case of inheritance Voter < User, is the 'users' table used by the Voter model? Or does it need another table? If so, Voter would be restricted to the User fields, in terms of data persistence. (Usually, inheritance is used to "add" to an existing class.) I've never understood inheritance very well.. so, any wisdom you can share is greatly appreciated.

Regards,
az

Re: Best approach for types of users?

azazel wrote:

1. What does STI stand for? Google didnt help on this.

It stands for Single Table Inheritance.

azazel wrote:

2. In the case of inheritance Voter < User, is the 'users' table used by the Voter model? Or does it need another table? If so, Voter would be restricted to the User fields, in terms of data persistence. (Usually, inheritance is used to "add" to an existing class.) I've never understood inheritance very well.. so, any wisdom you can share is greatly appreciated.

It uses the same table. You just need to add a string column to the table called "type" which stores the name of the subclass to use. It's good to use it when you have different types of models which need to behave differently but have the same data/attributes.

Railscasts - Free Ruby on Rails Screencasts

Re: Best approach for types of users?

Ryan, I got a problem.

I should have found this earlier when doing my model tests, but I'm coming across it now.

Let's say I do this in script/console

And that the id's are valid users.

m = Message.create(:author_id => 1, :recipient_id =>2)
m.author = user1 (good)
m.recipient = user2 (good)
user1.messages

"ActiveRecord::StatementInvalid in 'A new question should list all the messages for the user'
SQLite3::SQLException: no such column: messages.user_id: SELECT * FROM messages WHERE (messages.user_id = 2)
./spec/models/message_spec.rb:40:"


No good, I can't query the database for the user's messages? Arg, why is that?

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.

Re: Best approach for types of users?

Well, I assume your 'messages' table has a field called 'author_id' and another called 'recipient_id'. The problem you have is that 'user1.messages' is looking for messages whose 'user_id = 2', which doesn't exist.

You will probably need something like this inside you 'User' model:

has_many :messages, :class_name => 'Message', :foreign_key => :recipient_id

':foreign_key => :recipient_id' will tell the model how to construct the sql statement. Instead of '...WHERE messages.user_id = 2' (which is Rails default).. it will do '...WHERE messages.recipient_id = 2'.

Hope it works.

Last edited by azazel (2007-06-04 03:21:10)

Re: Best approach for types of users?

Yeah, sorry I didn't reply when I figured this out - I could have saved some typing. But I'm sure this could help some other person that runs into this 'gotcha'. Thanks  smile

http://danielfischer.com - Personal Web-Technology-Blog, Los Angeles.