User-editable to_param value - ruby-on-rails

I have a model that has an overriden to_param method returning the (unique) name of a record. This works quite fine, however with one caveat - the user can edit the name.
If I have record #1 with name="abc" and record #2 with name="xyz", then a user editing record #2 and changing name to "abc" will get an error upon saving as the validates_uniqueness_of constraint is violated. However when Rails constructs the edit.html.erb page again, it uses the unvalidated data - including to to_param which is now linking everything to record #1 ("abc"). Consequent saves thus act on record #1 instead of record #2.
What would be the recommended best practice to prevent this horrendous result? Should I reset the name value before redirecting upon an error (but what if the name was okay and the error was elsewhere) or should I change my views to manually insert the id instead of using the automatics of Rails?

Probably the easiest thing to do would be to not rely on the name attribute but instead another attribute that is hidden from the user.
eg. if you had a permalink:string column on the model you could do something like:
Class ModelName < ActiveRecord::Base
before_save :update_permalink
validates_presence_of :name
def to_param
permalink
end
private
def update_permalink
self.permalink = name.parameterize
end
end

Related

How to check for a table fields value uniqueness and if not uniq make it uniq

In order for my Rails 4 app to have vanity urls, I have a polymorphic Slug table (id, url). Then in my Article model I have the following to create the slug:
class Article < ActiveRecord::Base
has_many :slugs, as: :sluggable, dependent: :destroy
after_save :create_slug
def create_slug
return if !url_changed? || url == slugs.last.try(:url)
#re-use old slugs
previous = slugs.where('lower(url) = ?', url.downcase)
previous.delete_all
slugs.create!(url: url)
end
end
The #article.url is currently being set in the controller with:
#article.url = #article.title.parameterize
This works really well unless there is a conflict, meaning an article is being created with a title already in use. In that case Rails error's with:
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint
I'm thinking I need create_slug to check first if the value exists, if not then create the slug however, if the value does exist, append a -1 or -2 etc... to the end of the URL until the URL is unique. Does this sound right? What's the best way to handle/code for this scenario?
The old behavior was implemented in the special module. But at the moment it has not been released yet. So, if you want to restore old behavior you can switch friebdly_id in your Gemfile on Github and add sequentially_slugged to the modules list.
If you don't want to move for any gems then, A simplest thing for you is use the below code for making the URL's better....
def to_param
"#{id}-#{slug}"
end
You can update the method code and make urls what ever you want.

Rails_admin, Assigning Devise's current_user to model inside action

I have Devise Model Person which will log in and manage rails_admin app.
I have model called Model which has updater that was added by mongoid-history.
In order to set who updated my story I need to do something like this:
model = Model.new
model.updater = Person.first
model.save
According to this link to github, I can not set current_person, which created by Devise automatically. That's means that I need to set updater manually each time when something happens to my Model.
How to get current_person and set it to Model where rails_admin's action happens?
I only know that I need to write something to each action in initializers/rails_admin.rb
There's no access to current_user from the Model, because ActiveRecord models can be used independently from Rails apps, they can be used without logging in.
It is the controller that (often) requires the user to login and so has access to the user session and the current_user method. The controller must therefore do the work of setting the updater on the Model.
Perhaps you could set a callback method in the controller that acts on any action that modifies the model and sets the updater there.
I have found one solution for my question, but I don't know if it is good aproach or not.
If I want to assign my current_person in edit action I needed to write this:
config.model Model do
edit do
field :updater_id, :hidden do
visible true
def value
bindings[:view]._current_user.id
end
end
end
end
It createds hidden field which has asigned ID of current person which is logged in.
Mr.D's answer seems not to work (at least as of rails_admin 2.0.2); bindings doesn't have a :view key. The correct key is now :controller:
bindings[:controller].current_user # returns current User object
And if you not have a attribute to store this id and just need id to validate another operation, you can create a attribute to it:
config.model Model do
edit do
field :updater_id, :hidden do
visible true
def value
bindings[:view]._current_user.id
end
end
end
end
MODEL:
class TaxPlan < ApplicationRecord
attr_accessor :updater_id
validate :validate_updater_id
def validate_updater_id
yor code validation here
end
end

Updating association without saving it

I have a model:
class A < ActiveRecord::Base
has_many :B
end
And I want to reset or update A's B association, but only save it later:
a = A.find(...)
# a.bs == [B<...>, B<...>]
a.bs = []
#or
a.bs = [B.new, B.new]
# do some validation stuff on `a` and `a.bs`
So there might be some case where I will call a.save later or maybe not. In the case I don't call a.save I would like that a.bs stay to its original value, but as soon as I call a.bs = [], the old associations is destroyed and now A.find(...).bs == []. Is there any simple way to set a record association without persisting it in the database right away? I looked at Rails source and didn't find anything that could help me there.
Thanks!
Edit:
I should add that this is for an existing application and there are some architecture constraint that doesn't allow us to use the the regular ActiveRecord updating and validation tools. The way it works we have a set of Updater class that take params and assign the checkout object the value from params. There are then a set of Validater class that validate the checkout object for each given params. Fianlly, if everything is good, we save the model.
In this case, I'm looking to update the association in an Updater, validate them in the Validator and finally, persist it if everything check out.
In summary, this would look like:
def update
apply_updaters(object, params)
# do some stuff with the updated object
if(validate(object))
object.save(validate: false)
end
Since there are a lot of stuff going on between appy_updaters and object.save, Transaction are not really an option. This is why I'm really looking to update the association without persisting right away, just like we would do with any other attribute.
So far, the closest solution I've got to is rewriting the association cache (target). This look something like:
# In the updater
A.bs.target.clear
params[:bs].each{|b| A.bs.build(b)}
# A.bs now contains the parameters object without doing any update in the database
When come the time to save, we need to persist cache:
new_object = A.bs.target
A.bs(true).replace(new_object)
This work, but this feel kind of hack-ish and can easily break or have some undesired side-effect. An alternative I'm thinking about is to add a method A#new_bs= that cache the assigned object and A#bs that return the cached object if available.
Good question.
I can advice to use attributes assignment instead of collection manipulation. All validations will be performed as regular - after save or another 'persistent' method. You can write your own method (in model or in separated validator) which will validate collection.
You can delete and add elements to collection through attributes - deletion is performed by additional attribute _destroy which may be 'true' or 'false' (http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), addition - through setting up parent model to accept attributes.
As example set up model A:
class A < ActiveRecord::Base
has_many :b
accepts_nested_attributes_for :b, :allow_destroy => true
validates_associated :b # to validate each element
validate :b_is_correct # to validate whole collection
def b_is_correct
self.bs.each { |b| ... } # validate collection
end
end
In controller use plain attributes for model updating (e.g update!(a_aparams)). These methods will behave like flat attribute updating. And don't forget to permit attributes for nested collection.
class AController < ApplicationController
def update
#a = A.find(...)
#a.update(a_attributes) # triggers validation, if error occurs - no changes will be persisted and a.errors will be populated
end
def a_attributes
params.require(:a).permit([:attr_of_a, :b_attributes => [:attr_of_b, :_destroy]])
end
end
On form we used gem nested_form (https://github.com/ryanb/nested_form), I recommend it. But on server side this approach uses attribute _destroy as mentioned before.
I finally found out about the mark_for_destruction method. My final solution therefor look like:
a.bs.each(&:mark_for_destruction)
params[:bs].each{|b| a.bs.build(b)}
And then I can filter out the marked_for_destruction? entry in the following processing and validation.
Thanks #AlkH that made me look into how accepts_nested_attributes_for was working and handling delayed destruction of association.

How to create permalink with id?

How would I create a permalink with an id for a new model?
E.g
animal = Animal.create(name: 'cool dog') #creates animal with id of 1 and name of dog
animal.permalink => "1-cool-dog"
How do you add the proper callback so that id is inserted? before_save or after_save doesn't work
after_save :update_permalink #or before_save
def update_permalink
self.permalink = "#{id} #{name}".parameterize
end
What ends up happening is I get "cool-dog" instead of "1-cool-dog"
And I get why. It's setting an attribute without saving it on after_save. But doesn't work on before_save either because id hasn't been created on a new record.
According to http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
You should use after_commit instead of after_save
Both save and destroy come wrapped in a transaction that ensures that
whatever you do in validations or callbacks will happen under its
protected cover. So you can use validations to check for values that
the transaction depends on or you can raise exceptions in the
callbacks to rollback, including after_* callbacks.
As a consequence changes to the database are not seen outside your
connection until the operation is complete. For example, if you try to
update the index of a search engine in after_save the indexer won’t
see the updated record. The after_commit callback is the only one that
is triggered once the update is committed. See below.
As I commented above you may want to simply override the to_param method of your Animal Model like this.
def to_param
"#{id}-#{name.parameterize}"
end
This will make all of your urls automatically like the permalink you are trying to create and you can still use Animal.find(params[:id])
Perhaps you don't need to save the permalink to the database at all.
def permalink
"#{self.id} #{self.name}"
end
This approach would add a permalink to the model by concatenating the id and name each time the permalink is read.

need to generate an object on create that contains the id

so i need to create an image that belongs to my model (string with the url of the image) in the models create method.
the problem is, that this image is a QR-Code that should contain the url of the object that gets created.
but the URL (of course) is unknown in the create method because no id exists at that point for the given object.
any ideas how to solve this problem?
I don't see an obvious way of doing this, beyond using a non id column within the URL (e.g. make a call to generate a UDID/GUID, and use that in the url http://mysite.com/obj/#{udid}), or saving in two stages, using the after_create callback to set the image once the record has been saved:
class MyModel < ActiveRecord::Base
after_create :set_image
def set_image
if image_attribute == nil
image_attribute = generate_a_qr_code(self)
self.save
end
end
end
Use two-pass saving :-)
def create
model = Model.new params[:model]
if model.save
# at this point you have an id
model.qr = generate_qr model
model.save
# proceed as usual
end
end
This is for traditional databases with auto-increment column as primary key. In some databases, keys are generated using sequences that you can query to get a new value (before saving your object). In some databases (MongoDB) keys can be generated on the client completely.

Resources