Topic: wanted: modifying an item creates a new one

I've got a new app I'm working on where a user may modify something, but instead of updating the record, I want it to create a new, related record.  For example, let's say a the site has a listing of colors and a user adds a color (e.g. green) to his profile.  Then, later, he edits that color so it's more his style (e.g. olive).

I'd like to still have green in my colors table but then also add a new color called olive to the colors table.  The tricky part is that the user would only have olive in his profile now but not green.

I was thinking about setting it up where uwer and color both has and belong to each other.  Then if a user modifies the color, I create a new color (with perhaps a parent_id of the orignal color) and modify the join table.  Does that sound about right?  Is there a better/easier way to tackle it?

Re: wanted: modifying an item creates a new one

I think you're on the right track. Having a separate colors table sounds like the way to go.

Railscasts - Free Ruby on Rails Screencasts

Re: wanted: modifying an item creates a new one

Also, when you do come to implement it there's a useful dynamic finder find_or_create_by_. So you can do something like:

color = Color.find_or_create_by_name(params[:user][:color])

Alex

Re: wanted: modifying an item creates a new one

Ok, so I've got a color model:

 has_and_belongs_to_many :users,
    :class_name => "Color",
    :association_foreign_key => "user_id",
    :join_table => "colors_users"

and corresponding code in user's model:
  has_and_belongs_to_many :colors,
    :class_name => "User",
    :association_foreign_key => "color_id",
    :join_table => "colors_users"

Then in my controller, when somebody adds a color, I have:

current_user.colors << Color.find(params[:id])

But it gives me an error that says:
NoMethodError (undefined method `colors' for :false:Symbol):

Can anyone help?  What's the right way to create (and find) the association between the user and color?

Thanks in advance..

Re: wanted: modifying an item creates a new one

Hey there,

If you are using a User and a Color model, you would only need

class Color < ActiveRecord::Base
  has_and_belongs_to_many :users
end

class User < ActiveRecord::Base
  has_and_belongs_to_many :users
end


Rails assumes by defaut that you have a colors_sectors table and the color_id and sector_id columns when using a HABTM relationship. Moreover, when having the HABTM relationship you get a nifty attribute (attr_writer) for free. In your User model that would be color_ids and in your Color model user_ids. As you can imagine... that would be an array.

I have not tried your example, but why don't you try

# connection an existing color to the user
current_user.color_ids << params[:id]
current_user.save

# making a new color and connection it with the user
current_user.color.build(:name => 'yellow')


I can't promise if that will work, but... thinking a little more outside the box here: why don't you use acts_as_taggable as plugin for what you are trying to achieve? I know a Color is not particularly a Tag, but a tag by definition is a descriptive keyword for an item. So, a color describes the user, hence being a tag wink

If you have more than just the color that you want to describe your users with, you can use the same tags table. Simply extend the Tag model with an additional attribute like 'tag_type' where you can store 'color' or your other tag types. You'd simply have to tweak acts_as_taggable a little towards your needs and you'd save yourself a lot of work later on, maybe.

Last edited by rudionrails (2007-06-17 21:07:02)

Re: wanted: modifying an item creates a new one

Assuming that you want this behavior every time you save for the Color class, not just in some controllers/actions:

class Color < ActiveRecord::Base
  # other stuff

  def save
    new_record? ? super : Color.create(attributes.merge(:parent_id => id))
  end
end

#usage:
@color = Color.find_by_name("blue")
@color.id          # > 12 (just an example)
@color.name = "green"
@color.hex = "#00FF00"
@color.save
@color.id          # > 15 (again, example)
@color.parent_id   # > 12


If you only need to do this in a few places and the save functionality should be normal in other places, you could create a seperate method, maybe save_as_new that does almost the same thing as above, no need to check for new_method?. Alternatively, you could have the save method take an optional parameter:

class Color < ActiveRecord::Base
  # other stuff

  def save(new = false)
    new ? Color.create(attributes.merge(:parent_id => id)) : super
  end
end

Re: wanted: modifying an item creates a new one

I think maybe I didn't explain the problem well enough.. your solutions I think will help me too but first I have a small hurdle in saving the data to the colors_users table.

I would expect that, b/c of the HABTM relationship above, that calling:

current_user.colors << Color.find(params[:id])

would save the user_id of the current user along with the id of the param color, but it's not. (now I am getting an error saying that it's expecting User but got Color) Also, I populated some dummy data in that table, and I think I should be able to read it using code like

warn current_user.colors

But even though there's data in the table I'm still getting an empty set.  I think my model must be wrong..  I tried simplifying it to just has_and_belongs_to_many :users has_and_belongs_to_many :colors, but that didn't seem to affect anything..

Re: wanted: modifying an item creates a new one

What is current_user? Is it a method? Is it a variable? In any case, ruby is saying that current_user refers to a symbol, according to the error you posted:

undefined method `colors' for :false:Symbol

That makes me think that current_user is where the problem lies. If you can't figure it out, post the code that is relevant to current_user, and we can go from there.

Re: wanted: modifying an item creates a new one

I believe viniosity is using acts_as_authenticated? If so, current_user.colors should work.

try to output attributes of current_user:

# username for instance
current_user.name

# in case this works, try the colours of the user
# if the colors table has the attribute "name"
current.user.colors.collect{ |c| c.name }


If you are not using acts_as_authenticated for your user authentication, let us know how you assign current_user.

edit: just amended the little typing error wink

Last edited by rudionrails (2007-06-19 18:21:32)

Re: wanted: modifying an item creates a new one

I'm actually using restful authentication but it's the same behavior for current_user

Late last night I got it working. I messed with a bunch of code so I'm not sure exactly what did it.  I'm going to give credit to simplifying the model as suggested by rudionrails.  I could have sworn I had stopped my server and restarted but when I tried it earlier .. ? 

Anyway, I appreciate you all sticking with me through the thread, but I suppose we can consider it closed for the moment (until the time I start messing with the creation of the new color from the old).

Thanks guys!

Re: wanted: modifying an item creates a new one

fabio wrote:

class Color < ActiveRecord::Base
  # other stuff

  def save(new = false)
    new ? Color.create(attributes.merge(:parent_id => id)) : super
  end
end

I only need to do this when a specific customize action is called.  I have a special save method I'm calling alter where I've got:

def alter
  new ? Color.create(attributes.merge(:parent_id => id)) : super
end

But when I go to save, I get this error:

undefined local variable or method `attributes' for #<ColorsController:0x3629460>

I'm hoping it's simple.. do I maybe have a missing attr call in my model?

Re: wanted: modifying an item creates a new one

Where do you have the alter method? Is it in the controller or the model?

Re: wanted: modifying an item creates a new one

It's in the controller. Should it be in the model? 

I've got a controller method called customize which is called when I customize the color.  That view just has a call to the form like so:

<% form_for(:color, :url => alter_color_path(@color), :html => { :method => :put }) do |f| %>

My routes.rb has a line for the custom named route:
map.resources :colors, :collection => { :search => :get, :customize => :get }, 
                       :member => { :add_color => :post, :customize => :get, :alter => :put }

So what happens is similar to what happens when you call edit.. the edit function then calls update.  In this case, the customize function calls alter (both in the controller).

I have a workaround for it where I use a session[:color] to maintain the parent's id and just save it as a new one, but I think your solution would be much more elegant if I can get it working...

Re: wanted: modifying an item creates a new one

OK, here's what I would do:

In your Color Model, override the save method so that it works normally if you don't pass it any parameter, but creates a new color and saves it if you pass :as_new as the first parameter:

class Color < ActiveRecord::Base
  #...

  def save(opt = nil)   # take an optional parameter that tells if it should save as new
    # if the 'new' parameter is :as_new, save as new,
    # otherwise, just delegate to the old save method (super)
    opt == :as_new ? Color.create(attributes.merge(:parent_id => id)) : super
  end

  #...
end


Then, in your controller you could have the alter action use this new save(:as_new) functionality:

class ColorsController < ApplicationController
  #...

  def alter
    @color = Color.find(params[:id])
    @color.attributes = params[:color]
    @color.save(:as_new)
    render :action => :list   # or whatever
  end

  #...
end

Last edited by fabio (2007-06-19 15:27:47)

Re: wanted: modifying an item creates a new one

fabio wrote:

OK, here's what I would do:

In your Color Model, override the save method...

Ohh! Are you sure you want to do it like this? Check out the ActiveRecord Callbacks(better known as before- and after filters). I am sure that's a better and safer way of doing it.

Like I mentioned before already, acts_as_taggable is doing exactly what you are looking for. get the plugin, browse through the files and see what it can help you with. (I hope I dont get into trouble with the author suggesting this).

Last edited by rudionrails (2007-06-19 18:52:13)