Update attribute in associated model in Rails when parent is updated - ruby-on-rails

I have two models User and Assignment. Whenever User is updated I want to update the url attribute in Assignment.
How do I do this?
class User
has_many :assignments
...
end
class Assignment
belongs_to :user
before_save :set_url
def set_sandbox_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
end

I agree with Tamer Shlash that there is not really a benefit in storing this URL in the database because it can be easily generated each time you need it.
But apart from that, I would like to answer your question. Your callback to regenerate the URL doesn't work for various reasons. First, you want to update the URL when the user changes therefore the user needs to have a callback defined. Second, the naming is not correct. The callback as it is currently written would try to run a set_url method but the method is actually called set_sandbox_url. And third, sandbox_url = will just assign the new URL to a local variable sandbox_url but it would not update the instance variable #sandbox_url.
I would do something like this:
# in app/models/user.rb
after_save :update_assignment_url
private
def update_assignment_url
assignments.each(&:update_url) if username_previously_changed?
end
# in app/models/assignments.rb
def update_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
update!(sandbox_url: sandbox_url)
end
Note: because you build the URL by simply concatenating strings I suggest making sure that these strings (especially values provided by the user like username and article_title) only include characters that are valid in an URL (for example by using String#parameterize).
You might want to read about Dirty Attributes too which provided the used username_previously_changed? method.

Related

How can I automatically create an adress parameter upon sign up with Devise (Ruby On Rails)

I'm currently trying to automatically create an user_address (which will be a randomly generated hash, which is for now hardcoded) string upon sign-up with the Ruby on Rails devise Gem. I have added the user_address to the list of allowed parameters I am currently trying to add this logic to the registrations_controller.rb by adding the following method :
def create
super do
current_user.user_address = '1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX'
end
end
I suppose this is because current_user must not be defined before the actual POST (create user) has been processed but I am not sure about the way to do this.
Any help would be greatly appreciated, thanks in advance
If i understand you correctly (I think I do) you could move away from trying to do this in the create action in the controller and instead use a model callback on the User model.. that way its automatically created when a user registers.
example:
class User < ApplicationRecord
before_create :assign_user_address
validates_uniqueness_of :user_address
def assign_user_address
begin
self.user_address = SecureRandom.hex
other_user = User.find_by(user_address: self.user_address)
end while other_user
end
end
the before_create will generate the user_address and assign it to the user that is registering, while the validates_uniqueness_of ensures that you will never have a duplicate user address all in one fell swoop (although with a random string the chances of duplicating are slim.. but better to be safe than sorry.. I find this method super easy and keeps your controller clean and un cluttered.
Let me know if this wasn't what you were looking for..

Getting methods that work in controller/views to function in a model, Ruby on Rails

I'm trying to add a string to the user model under a location column, based on the user's location. I have everything setup to the point that I know the value of #city+#state is added to the appropriate column in the correct model. The problem is, it appears that request.location.city and request.location.state function properly in the controller and views, but not in the model.
def add_location
#city = request.location.city
#state = request.location.state
#location = #city+#state
self.location = #location
end
When a user is created, rather than creating a string such as "losangelescalifornia", nothing is created. When I define #city = "bob" and #state = "cat", all users created have "bobcat" in the appropriate place. I know then that everything is functioning except these geolocation based methods. So my question is, how would I get these methods (correct me please if that is not what they are) to function in the model, being request.location.city and request.location.state? Many thanks in advance :)
I agree with Rudi's approach, mostly, but I'll offer a little more explanation. The concept you're wrestling with is MVC architecture, which is about separating responsibilities. The models should handle interaction with the DB (or other backend) without needing any knowledge of the context they're being used in (whether it be a an HTTP request or otherwise), views should not need to know about the backend, and controllers handle interactions between the two.
So in the case of your Rails app, the views and controllers have access to the request object, while your models do not. If you want to pass information from the current request to your model, it's up to your controller to do so. I would define your add_location as follows:
class User < ActiveRecord::Base
def add_location(city, state)
self.location = city.to_s + state.to_s # to_s just in case you got nils
end
end
And then in your controller:
class UsersController < ApplicationController
def create # I'm assuming it's create you're dealing with
...
#user.add_location(request.location.city, request.location.state)
...
end
end
I prefer not to pass the request object directly, because that really maintains the separation of the model from the current request. The User model doesn't need to know about request objects or how they work. All it knows is it's getting a city and a state.
Hope that helps.
request variable is not available in the model since it depends on the current HTTP request.
You have to pass to model as param.
def your_action
...
#model.add_location(request)
...
end
def add_location(request)
..
end

update_attributes field tweaks

So I've got an edit page that has butt-load of editable fields on it...simple update...
#patient.update_attributes(params[:patient])...everything's great, except....
I've got one field out of these 20 that I need to tweak a little before it's ready for the db and it would seem I either need to do
two trips
#patient.update_attributes(params[:patient])
#patient.update_attribute( :field=>'blah')
or set them all individually
patient.update_attributes(:field1=>'asdf', :field2=>'sdfg',:field3=>'dfgh', etc...)
Am I missing a way to do this is one swoop?
What's the attribute you need to tweak? There's two ways to do this:
Either massage the params before you send them to the update_attribute method:
I'm just giving an example here if you wanted to underscore one of the values:
params[:patient][:my_tweak_attribute].gsub!(" ", "_")
#patient.update_attributes(params[:patient])
Then there's the preferred way of doing your tweaking in a before_save or before_update callback in your model:
class Patient < ActiveRecord::Base
before_update :fix_my_tweak_attribute, :if => :my_tweak_attribute_changed?
protected
def fix_my_tweak_attribute
self.my_tweak_attribute.gsub!(" ", "_")
end
end
This keeps your controller clean of code that it probably doesn't really need.
If you just need to add a new param that didn't get sent by the form you can do it in the controller like this:
params[:patient][:updated_by_id] = current_user.id
#patient.update_attributes(params[:patient])
Assuming current_user is defined for you somewhere (again, just an example)
You can create a virtual attribute for that field. Say the field is :name. You create a function in your Patient model like :
def name
self[:name] = self[:name] * 2
end
And of course, you do your things inside that function :) Instaed of self[:name], you can also use read_attribute(:name).

How to setup default attributes in a ruby model

I have a model User and when I create one, I want to pragmatically setup some API keys and what not, specifically:
#user.apikey = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
I want to be able to run User.create!(:email=>"something#test.com") and have it create a user with a randomly generated API key, and secret.
I currently am doing this in the controller, but when I tried to add a default user to the seeds.rb file, I am getting an SQL error (saying my apikey is null).
I tried overriding the save definition, but that seemed to cause problems when I updated the model, because it would override the values. I tried overriding the initialize definition, but that is returning a nil:NilClass and breaking things.
Is there a better way to do this?
use callbacks and ||= ( = unless object is not nil ) :)
class User < ActiveRecord::Base
before_create :add_apikey #or before_save
private
def add_apikey
self.apikey ||= Digest::MD5.hexdigest(BCrypt::Password.create(self.password).to_s)
end
end
but you should definitely take a look at devise, authlogic or clearance gems
What if, in your save definition, you check if the apikey is nil, and if so, you set it?
Have a look at ActiveRecord::Callbacks & in particular before_validation.
class User
def self.create_user_with_digest(:options = { })
self.create(:options)
self.apikey = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
self.save
return self
end
end
Then you can call User.create_user_with_digest(:name => "bob") and you'll get a digest created automatically and assigned to the user, You probably want to generate the api key with another library than MD5 such as SHA256 you should also probably put some user enterable field, a continuously increasing number (such as the current date-time) and a salt as well.
Hope this helps
I believe this works... just put the method in your model.
def apikey=(value)
self[:apikey] = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
end

In Ruby on Rails, does changing to_param() have any side effect and what ways can be used to change it?

In some book, it is recommended that to_param is changed to
class Story < ActiveRecord::Base
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
so that the URL is
http://www.mysite.com/stories/1-css-technique-blog
instead of
http://www.mysite.com/stories/1
so that the URL is more search engine friendly.
So probably to_param() doesn't need to be used by other parts of Rails that changing it may have any side effect? Or maybe the only purpose is to construct a URL for linking?
Another thing is, won't it require to limit the URL size to be less than 2k in length -- will it choke IE if it is more than 2k or maybe the part more than 2k is just ignored by IE and so the URL still works. It might be better to be limited to 30 or 40 characters or something that will make the URL not exceedingly long.
Also, the ri doc of to_param:
class User < ActiveRecord::Base
def to_param # overridden
name
end
end
if to_param is changed like that, then the link actually won't work, as
http://www.mysite.com/stories/1-css-technique-blog
will work, but
http://www.mysite.com/stories/css-technique-blog
will not work as the ID is missing. Are there other ways to change the to_param method?
Update: on second thought, maybe
http://www.mysite.com/stories/css-technique-blog
won't work well if there are many webpages with similar title. but then
http://www.mysite.com/user/johnchan
will work. Will it be params[:id] being "johnchan"? So then we will use
user = User.find_by_login_name(params[:id])
to get the user. So it just depends on how we use the param on the URL.
C:\ror>ri ActiveRecord::Base#to_param
-------------------------------------------- ActiveRecord::Base#to_param
to_param()
------------------------------------------------------------------------
Returns a String, which Action Pack uses for constructing an URL to
this object. The default implementation returns this record's id as
a String, or nil if this record's unsaved.
For example, suppose that you have a User model, and that you have
a +map.resources :users+ route. Normally, +user_path+ will
construct a path with the user object's 'id' in it:
user = User.find_by_name('Phusion')
user_path(user) # => "/users/1"
You can override +to_param+ in your model to make +user_path+
construct a path using the user's name instead of the user's id:
class User < ActiveRecord::Base
def to_param # overridden
name
end
end
user = User.find_by_name('Phusion')
user_path(user) # => "/users/Phusion"
If you want to make your url more search engine friendly, you can use the friendly_id gem which makes exactly what you want. Is the easier way I've found to generate search engine friendly permalinks.

Resources