Forgive me if this is a basic question; I'm learning Rails (using 3.2) as I go.
My Event model has_many images. Each image has an is_primary boolean field. Event should have a cover_image method, which returns the image with is_primary set to true, or the first image otherwise. This is my code:
def cover_image
imgs = self.images
imgs.each { |i| return i if i.is_primary }
# If no primary
return imgs.first
end
I can't help feeling like there's a better way to do it, one that doesn't involve looping through all the elements just to find one.
You could do this with a scope quite easily:
Image model
scope :primary, where(:is_primary => true)
Event model
def cover_image
images.primary.first
end
This is a really basic example that should get you started at least, you'll want to build on it to handle a missing primary image gracefully, for example.
For completeness, you don't have to do this as a scope, if you'd prefer you can use where statements directly. Scopes are just really nice for staying DRY:
Event model
def cover_image
images.where(:is_primary => true).first
end
Related
Here's the problem: I'm using active record and returning some photo objects. The final consumer of these photo objects is going to be a mobile app.
The response needs to have thumbnail versions returned mobile developer has requested that the JSON coming back look like this..
{
"root_url":'http://place.s3.amazonaws.com/folder/',
"image_300":'image_300.jpg',
"image_600":'image_600.jpg',
"image_vga":'image_VGA.jpg',
"image_full":'image.jpg'
}
and not like this:
{
"root_url":'http://place.s3.amazonaws.com/folder/',
"thumbnails": {
"image_300":'image_300.jpg',
"image_600":'image_600.jpg',
"image_vga":'image_VGA.jpg',
"image_full":'image.jpg'
}
}
so far the easy way is to create columns for each of the thumbnails and wow it works. I don't like getting locked into that though because if we wanted different thumbnails later it would mean adding columns to the db etc. I would much prefer to either just specify the thumbnails in the model class OR have a separate table for thumbnails with one thumb per row of the table.
I've looked at delegate, composed_of, using GROUP_CONCAT in a join.., using :method=> in to_json .. none of these look like options. Is there an easy way to do this?
Basic model example:
class Photo < ActiveRecord::Base
has_many :thumbnails, :as => :thumbs_for #polymorphic
end
class Thumbnail < ActiveRecord::Base
# columns = name, filename
belongs_to :thumb_for, :polymorphic => true
end
So far the result looks like this based on the answer from jesse reiss
def as_json(options)
options ||= {} #even if you provide a default, it ends up as nil
hash = super(options.merge({:include => :thumbnails}))
if thumbs = hash.delete(:thumbnails)
thumbs.each {|t| hash.merge!({t['name']=>t['filename']})}
end
hash
end
You can customize the json serialization of an object pretty simply using the as_json method.
For this, you could do :
def as_json(*args)
hash = super(*args)
hash.merge!(hash.delete("thumbnails"))
end
Or you could do it super manually
def as_json(*args)
hash = super()
thumbnails.each do |thumb|
# build thumbnail json
end
end
You don't have to rely on ActiveRecord's super simplistic json serialization methods.
From the pointview of rails best practices, what is the best place to manipulate form data before saving?
For instace, on a contact form, I want to make sure that all data is saved in capitalized form ( don't you hate when PEOPLE SHOUT AT YOU in their "please contact me" form submission? :-) )
is it better to do manipulation in controller? I could either do it in create, or move it into some sort of private method , that will capitalize all string attributes of the object before saving / updating?
Or
is it better do in the model before_save?
It makes sense to me that it should be done in the model since I probably want that to be the same for all records, no matter whether I manipulate on them in a rake task or through the web interface.
Bonus:
Also where would I place it if I want that that on ALL my models, with the ability to override default on a case by case basis? Application controller?
There might be some special cases where you want to save value without capitalizing - i.e. brand name products that don't capitalize (i.e. utorrent) or a last name that should have multiple caps in the name (i.e. Irish & Scottish names like McDonald)
Thank you!
the easiest place to put this is in your model. I would suggest using either before_save or even before_validation if you feel that fits better. Something like this would do the trick:
before_save :upcase_content
def upcase_content
self.content = self.content.upcase
end
Additionally if you wanted to allow for exceptions of a case by case basis you could add an attr_accessor to your model.
class MyModel < ActiveRecord::Base
attr_accessor :dont_upcase
before_save :upcase_content, :unless => :dont_upcase
...
end
then when you create a model set the accessor to true
#model = Model.new(:brand_name => utorrent)
#model.dont_upcase = true
#model.save!
The best place to put this is in your model, that way you have a fat model and a skinny controller, which is a "good thing".
If you want to have this be available for all of your models my suggestion is to use a module which contains your shared functionality and then include that in all the models you want to have the default behavior.
Ok based on the suggestions from other replies I came up with this solution:
lib/clean_strings.rb
module ActiveRecord
class Base
attr_accessor :dont_capitlize, :dont_strip
before_save :_capitalize_strings, :unless => :dont_capitlize
before_save :_strip_whitespaces, :unless => :dont_strip
def _capitalize_strings
self.attributes.each_pair do |key, value|
self[key] = value.capitalize if value.respond_to?('capitalize')
end
end
def _strip_whitespaces
self.attributes.each_pair do |key, value|
self[key] = value.strip if value.respond_to?('strip')
end
end
end
end
in environment.rb addded
require "clean_strings"
Now whenever I do
#a.dont_capitalize = true
#a.save!
it cleans it before saving according to my rules ( it will strip whitespace, but not capitalize it ). Obviously it needs more fine tuning, but i think it's a good way to define format rules for commonplace things. This way I don't need to sanitize every and each form input for things like extra whitespaces, or people who don't know where the CAPS LOCK is !!!
Thank you all for your input ( all upvoted).
I'm trying to override the way rails apply and id to an associated object, for example:
There are 2 simple models:
class Album < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :album
end
And then I want to do this:
album = Album.new :title => 'First Album'
album.photos.build
album.save #=> true
On this case I've created a plugin that overrides the id property and replaces it to a hashed string, so what I want to do is find the methods where this album_id is being replaced for my custom method instead of the int and be able to converted before it's saved.
But I want to act globally inside Rails structure because since it will be a sort of plugin I want to make this action work on dynamic models, that's why I can't create an before_save validation on the model.
I'm not sure if it's easy to understand, but I hope someone could help me on that..
Here's a screenshot of my current table so you can see what is happening:
SQLite3 DB http://cl.ly/1j3U/content
So as you can see the album_id it's being replaced for my custom ruby object when its saved...I've disabled the plugin and then it saved normally with records 11 and 12...
I want just act on a rails action and converted with my custom methods, something like
def rails_association_replaced_method(record)
#take the record associations and apply a to_i custom method before save
super(record)
end
something like this :)
Well I hope this didn't get too complicated
Cheers
It seems if I only override theActiveRecord::Base save method do the job if handled properly
define_method 'save' do
int_fields = self.class.columns.find_all { |column| column.type == :integer }
int_fields.each do |field|
if self.attributes[field.name]
self.attributes[field.name] = self.attributes[field.name].to_i
end
end
super
end
And this shall replace all the integer fields from the Current Model applying a to_i method over the result.
Rails is unfriendly to that kind of change to the defaults. What's your end goal here?
Is there an easy or at least elegant way to prevent duplicate entries in polymorphic has_many through associations?
I've got two models, stories and links that can be tagged. I'm making a conscious decision to not use a plugin here. I want to actually understand everything that's going on and not be dependent on someone else's code that I don't fully grasp.
To see what my question is getting at, if I run the following in the console (assuming the story and tag objects exist in the database already)
s = Story.find_by_id(1)
t = Tag.find_by_id(1)
s.tags << t
s.tags << t
My taggings join table will have two entries added to it, each with the same exact data (tag_id = 1, taggable_id = 1, taggable_type = "Story"). That just doesn't seem very proper to me. So in an attempt to prevent this from happening I added the following to my Tagging model:
before_validation :validate_uniqueness
def validate_uniqueness
taggings = Tagging.find(:all, :conditions => { :tag_id => self.tag_id, :taggable_id => self.taggable_id, :taggable_type => self.taggable_type })
if !taggings.empty?
return false
end
return true
end
And it works almost as intended, but if I attempt to add a duplicate tag to a story or link I get an ActiveRecord::RecordInvalid: Validation failed exception. It seems that when you add an association to a list it calls the save! (rather than save sans !) method which raises exceptions if something goes wrong rather than just returning false. That isn't quite what I want to happen. I suppose I can surround any attempts to add new tags with a try/catch but that goes against the idea that you shouldn't expect your exceptions and this is something I fully expect to happen.
Is there a better way of doing this that won't raise exceptions when all I want to do is just silently not save the object to the database because a duplicate exists?
You could do it a couple of ways.
Define a custom add_tags method that loads all the existing tags then checks for and only adds the new ones.
Example:
def add_tags *new_tags
new_tags = new_tags.first if tags[0].kind_of? Enumerable #deal with Array as first argument
new_tags.delete_if do |new_tag|
self.tags.any? {|tag| tag.name == new_tag.name}
end
self.tags += new_tags
end
You could also use a before_save filter to ensure that the list of tags doesn't have any duplicates. This would incur a little more overhead because it would happen on EVERY save.
You can set the uniq option when defining has_many relation. Rails API docs says:
:uniq
If true, duplicates will be omitted from the collection. Useful in conjunction with :through.
(taken from: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001833 under "Supported options" subheading)
I believe this works...
class Tagging < ActiveRecord::Base
validate :validate_uniqueness
def validate_uniqueness
taggings = Tagging.find(:all, :conditions => {
:tag_id => self.tag_id,
:taggable_id => self.taggable_id,
:taggable_type => self.taggable_type })
errors.add_to_base("Your error message") unless taggings.empty?
end
end
Let me know if you get any errors or something with that :]
I have the following one to many associations. Document has many Sections and Section has many Items.
class Document < ActiveRecord::Base
has_many :document_sections, :dependent => :destroy, :autosave => true
has_many :document_items, :through => :document_sections
end
class DocumentSection < ActiveRecord::Base
belongs_to :document
has_many :document_items, :dependent => :destroy, :autosave => true
end
class DocumentItem < ActiveRecord::Base
belongs_to :document_section
end
Here is the params hash:
-
Parameters: {"commit"=>"Submit Document", "authenticity_token"=>"4nx2B0pJkvavDmkEQ305ABHy+h5R4bZTrmHUv1setnc=", "id"=>"10184", "document"=>{"section"=>{"10254"=>{"seqnum"=>"3", "item"=>{"10259"=>{"comments"=>"tada"}}}}, "comment"=>"blah"}}
I have the following update method...
# PUT /documents/1
# PUT /documents/1.xml
def update
#document = Document.find(params[:id])
# This is header comment
#document.comment = params[:document][:comment]
params[:document][:section].each do |k,v|
document_section = #document.document_sections.find_by_id(k)
if document_section
v[:item].each do |key, value|
document_item = document_section.feedback_items.find_by_id(key)
if document_item
# This is item comments
document_item.comments = value[:comments]
end
end
end
end
#document.save
end
When I save the document it only updates the document header comments. It does not save the document_item comments. Shouldn't the autosave option also update the associations.
In the log only the following DML is registered:
UPDATE documents SET updated_at = TO_DATE('2010-03-09 08:35:59','YYYY-MM-DD HH24:MI:SS'), comment = 'blah' WHERE id = 10184
How do I save the associations by saving the document.
I think I see what the problem is. I'm pretty sure that you cannot do the following:
# Triggers a database call
document_section = #document.document_sections.find_by_id(k)
And expect ActiveRecord to keep the association for autosaves. Instead, you should save the loaded records individually. Which of course would not be atomic.
I believe for autosave to work like you are thinking, you want to do something like this:
# untested...
#document.document_sections.collect { |s| s.id == k }.foo = "bar"
Notice that here I'm actually modifying a fake param foo in the array, instead of calling find_by_id, which will re-query the database and return a new object.
A third option you have is that you could of course, do what you had originally planned, but handle all the transactions yourself, or use nested transactions, etc, to get the atmoic saves. This would be necessary if your data was too large for array manipulation to work since autosave by it's natures triggers a load of all associated data into memory.
It all depends on your application.
Some clarifications on the underlying problem:
If you run the find_by_id method, you are asking ActiveRecord to return to you a new set of objects that match that query. The fact that you executed that method from an instance (document_sections) is really just another way of saying:
DocumentSection.find_by_id(k)
Calling it from an object instance I think is just some syntactic niceness that rails is adding on the top of things, but in my mind it doesn't make a lot of sense; I think it could be handy in some application, I'm not sure.
On the other side, collect is a Ruby Array method that offers a way to "slice" an array using a block. Basically a fancy foreach loop. :) By interacting with the document_sections array directly, you are changing the same objects already loaded into the containing object (#document), which will then be committed when you save with the special autosave flag set.
HTH! Glad you are back and running. :)