Ruby (Rails) Delegate attributes to another model's method? - ruby-on-rails

-EDIT-
After reading about the Delegate method from the first answer, my question is this, is it possible to delegate two different methods to another single method.
IE: I currently have: #photo.attachment.file.url, and #photo.attachment.height, and #photo.attachment.width
I'd like to be able to access all of these via #photo.file.url, #photo.file.height, #photo.file.width.
The reason for the syntax is Attachment is a model that uses Paperclip to manage files, Paperclip is generating the .file method (the model is called Attachment, the model uses Paperclip's has_attached_file :file).
-ORIGINAL QUESTION-
I was wondering about aliasing methods and attributes in Ruby (I think this is a general ruby question, although my application is in Rails 3):
I have two models: Photo has_one Attachment.
Attachment has "height" and "width" attributes, and a "file" method (from Paperclip).
So by default I can access bits of the Attachment model like so:
photo.attachment.width # returns width in px
photo.attachment.height # returns height in px
photo.attachment.file # returns file path
photo.attachment.file.url #returns url for the default style variant of the image
photo.attachment.file.url(:style) #returns the url for a given style variant of the image
Now, in my photo class I have created this method:
def file(*args)
attachment.file(*args)
end
So, now I can simply use:
photo.file # returns file path
photo.file.url # returns file url (or variant url if you pass a style symbol)
My question is, I was able to direct photo.attachment.file to just photo.file, but can I also map height and width to photo.file, so that, for the sake of consistency, I could access the height and width attributes through photo.file.height and photo.file.width?
Is such a thing possible, and if so what does it look like?

So what you are asking is that
photo.file --> photo.attachment.file
photo.file.url --> photo.attachment.file.url
photo.file.width --> photo.attachment.width
You can't solve this with delegates, because you want that file to mean different things based on what follows next. To achieve this you would need to reopen paperclip, which i would not recommend (because i believe the api is good the way it is).
The only way i can think of to solve this, is to add eliminate the file level too. Like so:
photo.width --> photo.attachment.width
photo.file --> photo.attachment.file
photo.url --> photo.attachment.file.url
This you could then solve by using a delegate for each of the wanted methods.
So you write
class Photo
delegate :width, :height, :file, :to => :attachment
delegate :url, :to => :'attachment.file'
end
Hope this helps.

You can use Rails 'delegate' method. Have a look at my answer for this question:
What is a more Ruby-like way of doing this command?

The simplest way that comes to mind is to delegate url method in attachment to file:
class Attachment < ActiveRecord::Base
delegate :url, :to => :file
end
This way you can call photo.attachment.url, photo.attachment.width, photo.attachment.height, which for me seems pretty consistent. You could optionally alias attachment to file - this way you'd get the exact method names you asked for (photo.file.width, photo.file.url), but I would not recommend that, because it seems confusing (calling an attachment "file").
class Photo < ActiveRecord::Base
def file
attachment
end
end

With plain Ruby you can use Forwardable:
require 'forwardable'
class RecordCollection
attr_accessor :records
extend Forwardable
def_delegator :#records, :[], :record_number
end

Related

Simulate a property `MyClass.MyObjet.MyProperty` to replace an old call

I changed some references in my Rails model to stop using some behaviour (coming from CarrierWave). I don't want to break my old code. I want the references to stay the same.
My actual call looks like: Photo.picture.url, where picture was a model by itself. The Picture class is now deprecated, and replaced by a property with the same name in the Model.
I want to use the Photo.picture.url syntax to access my picture property and URL sub-attribute, defined directly in the Photo model.
Here's my code:
class Photo < ApplicationRecord
##============================================================##
## Associations
##============================================================##
belongs_to :photoable, polymorphic: true
acts_as_list scope: [:photoable_id, :photoable_type]
## This is what Im really not sure how to do it
def picture.url
"#{self.name}"
end
end
When I call the Photo.picture, I have a NoMethodError. I want to still use the same call without breaking the references.
How can I define my property in my model so my old syntax still works?
you can do something like
Picture = Struct.new(:url)
def picture
#picture ||= Picture.new(name)
end

RoR Carrierwave: Add remote files with multiple upload

I'm using the possibility to upload multiple files at once with carrierwave as described here:
https://github.com/carrierwaveuploader/carrierwave#multiple-file-uploads
So I have
class Item < ActiveRecord::Base
mount_uploaders :images, ImagesUploader
serialize :images, JSON
...
end
Now I want to upload a remote file (not from local drive). Usually I would use something like this in my controller
class ItemsController < ApplicationController
...
item.remote_images_url = params[:image_url]
...
end
But the helper remote_images_url (mind the plural version remote_images_url) only give me
undefined method remote_images_url
I also tried remote_image_url which would be the default helper in case of single file upload. Also undefined method.
How can I upload remote files when using "multiple files upload" with carrierwave?
Eventually I had a look into the github repository of carrierwave and I found this:
https://github.com/carrierwaveuploader/carrierwave/blob/master/lib/carrierwave/mount.rb
Luckily it's all described in the comments in this file. There was only one little thing I had to change. In the comments it says that for a column called images I had to use the helper method remote_image_urls. But actually I have to use remote_images_urls (always plural).
So that's the solution in my case:
class Item < ActiveRecord::Base
mount_uploaders :images, ImagesUploader
serialize :images, JSON
...
end
class ItemsController < ApplicationController
...
item.remote_images_urls = [params[:image_url]]
...
end
Mind the right plural types in the helper method as well as the surrounding array for the params (carrierwave excpects an array).
You need to use item.remote_images_urls not item.remote_images_url and assign the array of remote url. This works for me although CarrierWave rubydoc suggests me to use item.remote_image_urls . So, maybe you should try one of these .
For reference CarrierWave rubydoc

How to dynamically determining carrierwave uploader column name?

I have some meta-programming that condenses multiple direct S3 upload processes into a single controller. Then, those actions are shared across many models. To accomplish this, among many other details, I need to know the column name of a given models uploader. I facilitate this by having an 'uploader_name' method in each of my models that use the shared actions. For example, I have an Expense model that has a receipts uploader...
def uploader_name
'receipt'
end
mount_uploader :receipt, ReceiptUploader
So, now I can call Expense.new.uploader_name to return a string, or in a shared action I would use #obj.send(#obj.uploader_name) to get the uploader object (where #obj is any one of the affiliated models using the shared actions). This works fine. However, I think I can clean it up. One way that would help me refactor is not needing the uploader_name method. Or, being able to use a single uploader_name method in a shared module that is able to dynamically determine the column name of the uploader.
Is there a method within Carrierwave where I can access the column name on a models uploader without know what the uploader is called? Since the module is shared across multiple classes, i have to figure it out dynamically---I have read through the carrierwave, but not finding a solution.
Something like this...for example:
obj = self.class.name
obj.new.uploader # would return the mounted :receipts uploader
obj.new.uploader_column # would return the uploader column, in this case :receipt
There are a few methods provided by the uploader class. So, to solve this I just delegate the :mounted_as method from the uploader to the class that calls it. Then, without knowing the name of the actual uploader I can just call self.mounted_as.to_s to return the name of the uploader.
So, for a User model with an avatar uploader I do this.
class User
mount_uploader :avatar, AvatarUploader
delegate :mounted_as, to: :avatar
So, if I call #user.mounted_as I get :avatar. Or, more importantly...if I call self.mounted as I get :avatar when self is a User. I have a module that is shared between all models that use an uploader. In that module I include
def uploader_name
self.mounted_as.to_s
end
Now, I can do some meta-programming since I do not know who self is, and I do not know what self's uploader is called. But, now I can get access to both dynamically. This is helpful because I can share one uploader controller and one uploader form across multiple classes who use an uploader.
I can also do things like pass accepted extensions to the form input (see html5 accepts option) by simply grabbing the extensions in the uploader's extension_whitelist method.
self.send(uploader_name).extension_whitelist
# this is dynamically getting #user.avatar.extension_whitelist
# or #company.logo.extension_whitelist
I can also set uploader attributes using, for example...
obj.send(obj.uploader_name).content_type = 'image/jpeg'
# this is dynamically setting #user.avatar.content_type = 'image/jpeg'
# or #company.logo.content_type = 'image/jpeg'
obj.class.uploaders.keys.first
=> :receipt

Paperclip - dropbox. How can I setup a conditional has_attached_file?

I am creating an application that lets users store data in their personal dropbox in a protected folder that the application controls. So each user needs to store and access files in their own personal dropbox account.
To do this I'd like to leverage the paperclip-dropbox gem for storage. It allows paperclip to directly upload to dropbox: https://github.com/janko-m/paperclip-dropbox.
Here is the code that sets the authorization information for the paperclip-dropbox gem. NOTE: current_user does not work at the moment. I'm just putting that there to outline what would need to happen for the current setup to work.
Image.rb
has_attached_file :avatar,
:storage => :dropbox,
:dropbox_credentials => {app_key: DROPBOX_KEY,
app_secret: DROPBOX_SECRET,
access_token: current_user.token,
access_secret: current_user.secret,
user_id: current_user.uid,
access_type: "app_folder"}
Notice the dropbox authentication requires the current_user to get that particular set of credentials.
I know that the current_user is not supposed to be accessed from the model and I'd like to keep it that way so could anyone help me figure out how to do that with this current setup? Or suggest a better alternative?
Basically, I need to conditionally change the access_token, access_secret, & user_id on a per user basis.
Thanks!
I'm going to answer my own question because the other answers were too vague to accept - although they were on the right path. I think the community would prefer an answer with more code to back it up.
So here goes. To change the has_attached_file on a dynamic basis, you have to have a user_id column in the attachment model so that you're not calling current_user (which isn't possible without ugly hacks). Then you need a belongs_to as well to complete the user association. Let's assume I'm attaching an audio file to a Song model for this example.
The key to getting the dynamically set variables is to initialize the attachment with the after_initialize callback.
Song.rb
belongs_to :user
has_attached_file :audio
after_initialize :init_attachment
def init_attachment
self.class.has_attached_file :audio,
:storage => :dropbox,
:dropbox_credentials => {app_key: DROPBOX_KEY,
app_secret: DROPBOX_SECRET,
access_token: self.user.token,
access_token_secret: self.user.secret,
user_id: self.user.id
access_type: "app_folder"},
:dropbox_options => {}
end
You are, of course, free to setup your association differently, but this is a working code example for the question posed.
Just found this resource you might gain benefit from: Ruby on Rails - Paperclip and dynamic parameters
More specifically for you, I thought this might shed some light onto what you're doing:
# AssetsController
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.build(params[:asset])
#asset.uploaded_by = current_user
respond_to do |format|
# all this is unrelated and can stay the same
end
end
Notice the "#asset.uploaded_by" is set in the controller? Maybe you could pass similar variables to your model? I don't know how I would do it specifically, but you'd basically be able to set the save options before you try and save the file, giving you the ability to dynamically set the options
I think first thing you should do is to set association Image.belongs_to :user - you could then use simply user.token etc. instead of referencing current_user.
Now the hard part. You can't simply type:
access_token: user.token
because self is Image class which simply doesn't respond to user method (it's instance method). My idea is to modify this gem so it could accept lambdas as arguments with attachment instance (for example) passed to this lambda on call. The problem is I don't know if it's hard to modify this gem that way yet.

Ruby on Rails - Overriding the association id creation process

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?

Resources