Rails how to copy all active storage attachments to new object? - ruby-on-rails

I have a function to clone records in a rails application. In addition to the form data I would like to copy/attach any active storage file uploads that are attached to the source object to the new object. Any ideas on how to do this? Here is my action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.dup
render 'new'
end
class Compitem < ApplicationRecord
belongs_to :user
has_many_attached :uploads, dependent: :destroy
end

I ended up getting this working by using the https://github.com/moiristo/deep_cloneable gem. Final action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.deep_clone(include: :uploads_blobs)
#compitem.save
render 'new'
end

Just did this in one of my applications - it was a has_one rather than has_many but I think something like this should work for you, without adding any additional dependencies, in Rails 6+:
#compitem = #source.dup
#source.uploads.each do |original_file|
#compitem.uploads.attach(io: StringIO.new(original_file.download),
filename: original_file.filename,
content_type: original_file.content_type)
end
#compitem.save

Related

Rails 5 + Shrine + Polymorphic model + Pretty Location

I'm using Shrine for direct uploads to S3 and I'm trying to user the pretty_location plugin to set the location in my S3 bucket.
I have a document model has the file_datatext attribute and is connected to a FileUploader:
class Document < ApplicationRecord
belongs_to :documentable, polymorphic: true
include FileUploader[:file]
validates :file, presence: true
end
Other models are associated with the document model through the following concern:
module Documentable
extend ActiveSupport::Concern
included do
has_one :document, as: :documentable, dependent: :destroy
accepts_nested_attributes_for :document, allow_destroy: true
end
end
This is my FileUploader:
class FileUploader < Shrine
Attacher.promote { |data| PromoteJob.perform_later(data) }
Attacher.delete { |data| DeleteJob.perform_later(data) }
plugin :upload_options, cache: {acl: "public-read"}
plugin :upload_options, store: {acl: "public-read"}
plugin :logging, logger: Rails.logger
plugin :pretty_location
plugin :processing
plugin :delete_promoted
plugin :recache
plugin :restore_cached_data
plugin :delete_raw
plugin :validation_helpers
def generate_location(io, context = {})
# do something depending on context[:record].documentable
end
end
When uploading files from the user's filesystem via the client browser through nested attributes all works as expected and I'm able to generate a pretty location in my S3 bucket.
However, I have another model where I am trying to upload to S3 a PDF file which is generated in the backend with the following setup.
class Invoice < ApplicationRecord
has_one :documents, as: :documentable, dependent: :destroy
end
The Invoice model doesn't use the concern as I want it to connect to the polymorphic document with a has_many association.
class Api::V1::InvoicesController < Api::V1::BaseController
def upload_pdf
pdf = InvoicePdf.new(#invoice)
attachment = pdf.render
file = StringIO.new(attachment)
file.class.class_eval { attr_accessor :original_filename, :content_type }
file.original_filename = "invoice_#{#invoice.reference}.pdf"
file.content_type = "application/pdf"
#document = #invoice.documents.new(file: file)
if #document.save
render "documents/show.json", status: :created
else
render json: { errors: #document.errors }, status: :unprocessable_entity
end
end
end
The upload works fine and I am able to upload the PDF to my S3 bucket, but I am not able to generate a pretty location because when I'm inside the generate_location method the context[:record] both the documentable_type and the documentable_id are nil.
This is a strange behaviour as in the rails console I am able to see that the association is correctly set after the upload has been done (without pretty_location) by running Invoice.last.documents.file.url.
I have tried creating the document record in different ways, have tried using the same documentable concern that works for other models but the result is alway the same and I have run out of ideas.
Does anyone have a clue why the documentable_type and documentable_id are not being passed into the context object inside the FileUploader?
The above setup actually works. I was using a breakpoint inside the generate_location FileUploader method and the api was breaking because that method was returning nil.
After fixing that, the first time it ran documentable was still nil but the method would run a second time with the documentable attributes present.
def generate_location(io, context = {})
return "" unless context[:record].documentable
path = if context[:record].documentable_type == "SomeClass"
# do something
elsif context[:record].documentable_type == "OtherClass"
# do something else
else
# do something else
end
return path
end

rails 5 API low-level caching

I am a bit confused regarding Rails API caching. I am using JSONAPI spec and fast_jsonapi gem and trying to cache the vehicle itself on show action and if there are params coming over like include=service_notes,service_alerts then I would like to cache those too. This is my initial approach but not sure if it is right.
I have 2 main issues:
For the vehicle caching itself is there a better approach than my vehicle = Vehicle.find_cached(params[:id]). This is not using updated_at but an after save callback to update the cache if vehicle has been updated. I just don't see if I could somehow use sth like Rails.cache.fetch(["vehicles", vehicle], version: vehicle.updated_at) as it is proposed here: https://github.com/rails/rails/pull/29092 since this needs the vehicle instance. As you see the set_vehicle controller method is pretty awkward.
Does this Rails.cache.fetch(['vehicles', vehicle, include_params], version: vehicle.updated_at) make any sense? I am trying to cache the query based on the different include params. Maybe it is overkill and I could just include everything and cache it that way like:
Rails.cache.fetch(['vehicles', vehicle, 'with_includes'], version: vehicle.updated_at) do
Vehicle.includes(:vehicle_alerts, :service_notes, :service_intervals).find(params[:id])
end
What is the correct way to handle caching here?
service_note.rb setup same for service_interval.rb and vehicle_alert.rb
class ServiceNote < ApplicationRecord
belongs_to :vehicle, touch: true
end
vehicle.rb
class Vehicle < ApplicationRecord
after_save :update_cache
has_many :vehicle_alerts, dependent: :delete_all
has_many :service_notes, dependent: :delete_all
has_many :service_intervals, dependent: :delete_all
def update_cache
Rails.cache.write(['vehicles', vehicle_id], self)
end
def self.find_cached(vehicle_id)
Rails.cache.fetch(['vehicles', vehicle_id]) { find(vehicle_id) }
end
end
vehicles_controller.rb
before_action :set_vehicle, only: [:show]
def show
render json: VehicleSerializer.new(#vehicle, options).serialized_json
end
private
def set_vehicle
vehicle = Vehicle.find_cached(params[:id])
#vehicle = Rails.cache.fetch(['vehicles', vehicle, include_params], version: vehicle.updated_at) do
Vehicle.includes(include_params).find(params[:id])
end
authorize #vehicle
end
vehicle_serializer.rb (with fast_jsonapi gem)
# same for :service_notes and :vehicle_alerts
has_many :service_intervals do |vehicle, params|
if params[:include] && params[:include].include?(:service_intervals)
vehicle.service_intervals
end
end

Rails includes cache not being used in model method

So in a rails-api I'm working on, we're currently trying to optimize some of the longer running calls, and I'm having an issue with the .includes functionality. I've got it working in most situations, but there's one particular situation where it's not working in the way that I want it to.
Here's an example:
User class
class User < ActiveRecord::Base
has_many :images
has_one :active_image, -> { where(images: { active_image: true })}, class_name: 'Image'
has_many :facebook_auth
def get_profile_image
if active_image
active_image.image.url(:profile)
else
facebook = facebook_auth.last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
end
Controller:
class UserController < BaseAPIController
def get_user_image
user_id = params[:user_id]
user = User.includes(:active_image, :facebook_auth).find(user_id)
render json: user.get_profile_image
end
end
With this, I would assume that the .includes(:active_image, :facebook_auth) would cache the data so that when I call them in the get_profile_image method, it doesn't make any more db calls, but this isn't the case. What am I doing wrong here?
Thanks,
Charlie
You where almost there!
Try this approach:
class User < ApplicationRecord
has_many :images, dependent: :destroy
has_one :active_image,
-> { where(active: true) },
class_name: 'Image'
has_many :facebook_auths, dependent: :destroy
has_one :active_facebook_auth,
-> { order("created_at desc") },
class_name: 'FacebookAuth'
scope :eager_load_image_data,
-> { includes(:active_image).includes(:active_facebook_auth) }
def profile_image_url
if active_image
active_image.url
elsif active_facebook_auth
"https://graph.facebook.com/#{active_facebook_auth.provider_user_id}/picture?width=150&height=150"
else
nil
end
end
end
Then in your controller or whenever you want to eager load images:
# for one user, with id 2:
User.eager_load_image_data.find(2).profile_image_url
# for a collection (using 'all' here):
User.eager_load_image_data.all.map{ |user|
[user.name, user.profile_image_url]
}
This way the image data is eagerloaded, both from the Image class and the FacebookAuth class.
There where also some other issues in your method User#get_profile_image that I have fixed:
It always returns nil. I am sure in your real code you have early returns.
For collections, it does a N+1 query if looking for facebook_auth_tokens.
Well, I wanted to comment, but couldn't put code into the comments, so I'm giving a non-answer...
I don't see anything obviously wrong, but as a work around, you could do this in User or somewhere:
def self.user_profile_image(user_id)
active_image = Images.where(user_id: user_id).where(active_image: true).first
if active_image
active_image.image.url(:profile)
else
facebook = FaceBookAuth.where(user_id: user_id).last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
And just call/cache the image in your controller, if that's not overly simplistic...
def get_user_image
render json: User.user_profile_image(params[:user_id])
end
That makes at most 2 relatively efficient queries. It doesn't needlessly load user, etc.

Rails nested form - refactor create action | cocoon gem

Everything is working fine but I want to change the code in create action to something like in update action. Right now, in the create action I am looping through all the values and saving them, but want to do it in a single line.
I have a College model.
class College< ActiveRecord::Base
has_many :staffs, dependent: :destroy
accepts_nested_attributes_for :staffs, reject_if: :all_blank, allow_destroy: true
end
And this is my Staff.rb
class Staff < ActiveRecord::Base
belongs_to :college
end
And these are my Staffs controller create and update actions
def create
#college= College.find(params[:college][:id_college_profile]
)
staff_params = params[:college][:staffs_attributes].values
staff_params.each do |staff_param|
#staff = #college.staffs.new
#staff.name = staff_param[:name]
#staff.designation = staff_param[:designation]
#staff.experience = staff_param[:experience]
#staff.specialization = staff_param[:specialization]
#staff.save
end
redirect_to dashboard_path(id: #college.id), notice: "Successfully added Staff."
end
def update
#college= College.find(params[:college][:id_college]
)
#college.update_attributes(staff_parameters)
redirect_to root_path
end
These are strong parameters
def staff_parameters
params.require(:college).permit(:id, staffs_attributes: [:id, :name, :specialization, :experience, :designation, :_destroy])
end
Is there a way to save all of staffs in create action, without looping through all the values, but save all of them with a single line of code as in update action?
I have tried this in the StaffsController create action
def create
#college= College.find(params[:college][:id_college]
)
#staff= #college.staffs.new(staff_parameters)
#staff.save
redirect_to dashboard_path(id: #college.id), notice: "Successfully added Staffs."
end
But it threw this error
unknown attribute 'staffs_attributes' for Staff.
Can someone kindly help me with this issue?
This is a CollegesController so I am assuming the create action also creates the new college?
So in that case your create action should simply be something like:
def create
#college = College.new(staff_parameters)
if #college.save
# succesfully created
else
# there was a validation error
end
end
Note that in general we would use college_parameters because the root element is college and that you not only edit the nested staff, but also possibly attributes from college.
If the college always already exists (because you are doing a find), it is a bit confusing to me what the difference is between create and update and why not always render the edit action in that case?
I have a demo-project show-casing cocoon and nested attributes.
You can do this many ways. The "staff_parameters" method threw an error because you are calling it on class Staff in the create action and on the college class for the update action. Simplest thing to do what you want is to copy the staff parameters method strong parameters and duplicate it. Name this second method create_staff and change the "params.require(:college)" part to "params.require(:staff)" and leave the rest the same. Then in your create action you can do "college.staff(create_staff)". Im on my phone so the formatting isnt good lol i put the code in quotes.

Model blank due to carrierwave gem - nested_attributes

I've just started to learn Ruby and Ruby on Rails, and this is actually the first time I really have to ask a question on SO, is really making me mad.
I'm programming a REST api where I need to receive an image url and store it in my db.
For this, I've done a model called ImageSet that uses carrierwave to store the uploaded images like this:
class ImageSet < ActiveRecord::Base
has_one :template
mount_uploader :icon1, Icon1Uploader
mount_uploader :icon2, Icon2Uploader
def icon1=(url)
super(url)
self.remote_icon1_url = url
end
def icon2=(url)
super(url)
self.remote_icon2_url = url
end
end
This icon1 and icon2 are both received as urls, hence the setter override, and they can't be null.
My uploader classes are creating some versions with a whitelist of extensions and a override of full_name.
Then, I have this template class that receives nested attributes for ImageSet.
class Template < ActiveRecord::Base
belongs_to :image_set
accepts_nested_attributes_for :image_set
(other stuff)
def image_set
super || build_image_set
end
end
This model has a image_set_id that can't be null.
Considering a simple request, like a post with a json:
{
"template":
{
"image_set_attributes":
{
"icon1": "http....",
"icon2": "http...."
}
}
}
It gives always : ImageSet can't be blank.
I can access temp.image_set from the console,if temp is a Template, and I can set values there too, like, temp.image_set.icon = 'http...' but I can't seem to figure out why is it breaking there.
It should create the image_set, set its attributes a save it for the template class, which would assign its id to the respective column in its own model-
My controller is doing:
(...)
def create
#template = Template.create(params)
if #template
render status: 200
else
render status: 422
end
end
private
def params
params.require(:template).permit(image_set_attributes: [:id, :icon1, :icon2])
end
(...)
Hope you can give me tip on this one.
Thanks!
accepts_nested_attributes doesn't work as expected with belongs_to.
Don't use accepts_nested_attributes_for with belongs_to
Does accepts_nested_attributes_for work with belongs_to?
It can be tricked into working in certain circumstances, but you're better off changing your application elsewhere to do things the "rails way."
Templates validate_presence_of :image_set, right? If so, the problem is that this means ImageSets must always be created before their Templates, but accepts_nested_attributes thinks Template is the parent, and is trying to save the parent first.
The simplest thing you can do is switch the relationship, so Template has_one :image_set, and ImageSet belongs_to :template. Otherwise you're going to have to write some rather odd controller logic to deal with the params as expected.

Resources