Rails API Image Upload with Active Storage / S3, React and ActiveAdmin - ruby-on-rails

I have created an app using a Rails API with a React front-end and ActiveAdmin as the CMS back-end, which is based off of this tutorial. I am adding on a model which includes an image using ActiveStorage for file uploads, as well as S3 for storage on production.
app/models/photo.rb
class Photo < ApplicationRecord
has_one_attached :image
end
app/controllers/photos_controller.rb
class PhotosController < ApplicationController
include ActionController::Serialization
before_action :set_photo, only: [:show, :update, :destroy]
# GET /photos
def index
#photos = Photo.all
render json: #photos.to_json
end
...
def photo_params
params.require(:photo).permit(:image, :location_name, :region, :url)
end
end
app/serializers/photo_seralizer.rb
class PhotoSerializer < ActiveModel::Serializer
attributes :id, :image, :location_name, :region, :url
def image
rails_blob_path(object.image, only_path: true) if object.image.attached?
end
end
When I view the API endpoint, the attached image is not showing in the photo object, here is an example return. Is there something I'm missing in the Serializer, that isn't adding in the related image? Any help would be greatly appreciated, thanks!
[{
"id":1,
"location_name":"Acreage Ciderhouse",
"region":"Northern Metro",
"url":"https://example.com/acreage-ciderhouse/",
"created_at":"2020-01-09T15:18:33.298-07:00",
"updated_at":"2020-01-09T15:27:40.594-07:00"
}]

The activestorage saves data different, I think your index is calling data from your model table, but images are saved in relationship to other tables. So following is way to check if image is really saved use rails console to check.
rails c
#photo = Photo.find(1)
puts #photo.image.attached?
If you get true that image is saving properly now you can do following to add all image urls in your request
def index
#photos = Photo.all
#photos.each do |p|
if #photo.present?
image_url = { :image => (url_for p.image) }
p.attributes.merge(image_url)
end
end
render json: #photos.to_json
end
I did not test above code, just wrote, but I used it a lot for my apis.

Related

Using Rails Path and URL Helpers with fast_jsonapi

I would like to use the rails URL helper instead of hard coding the path to access the article.
I checked into the documentation but nothing is specified.
The article_path helper method exists (I checked by running rake routes)
class V3::ArticlesController < Api::V3::BaseController
def index
articles = Article.all
render json: ::V3::ArticleItemSerializer.new(articles).serialized_json
end
end
class V3::ArticleItemSerializer
include FastJsonapi::ObjectSerializer
attributes :title
link :working_url do |object|
"http://article.com/#{object.title}"
end
# link :what_i_want_url do |object|
# article_path(object)
# end
end
What you want to do is pass in the context to your serializer from your controller:
module ContextAware
def initialize(resource, options = {})
super
#context = options[:context]
end
end
class V3::ArticleItemSerializer
include FastJsonapi::ObjectSerializer
include ContextAware
attributes :title
link :working_url do |object|
#context.article_path(object)
end
end
class V3::ArticlesController < Api::V3::BaseController
def index
articles = Article.all
render json: ::V3::ArticleItemSerializer.new(articles, context: self).serialized_json
end
end
You should also switch to the jsonapi-serializer gem which is currently maintained as fast_jsonapi was abandoned by Netflix.
I found a solution thanks to max's example.
I also changed the gem to jsonapi-serializer
class V3::ArticlesController < Api::V3::BaseController
def index
articles = Article.all
render json: ::V3::ArticleItemSerializer.new(articles, params: { context: self }).serialized_json
end
end
class V3::ArticleItemSerializer
include JSONAPI::Serializer
attributes :title
link :working_url do |object|
"http://article.com/#{object.title}"
end
link :also_working_url do |object, params|
params[:context].article_path(object)
end
end

Deleting a single ActiveStorage attachment out of a collection via an API call

EDIT: TLDR: This boils down to serializing the attachments. See my response.
I can see two ways to achieve this:
(1) Serialize the attachments (with id and url attributes), thus providing an id to the FE that they can use to DELETE /attachments/:id which would then call ActiveStorage::Attachment.find(:id).purge. The problem is with serializing as attachments do not have built-in models. I tried creating an ActiveStorageAttachment model for the active_storage_attachments table but could not get the url for the attachment as the Rails.application.routes.url_helpers.url_for(#object) requires an ActiveStorage::Attachment object not an ActiveStorageAttachment object.
(2) Another option would be to have a DELETE /attachments/:attachment_url endpoint. For this to work, I'd need to get the ActiveStorage::Attachment object based on the url, in order to call purge on it. Not sure if that is possible?
I'd much prefer the first solution, it feels cleaner and more adaptable. Any help with either approach would be much appreciated!
In the end, I managed to serialize the attached images (option(1) above) using the jsonapi-rb gem.
In the controller, I include: :images and pass in the attachment type using the expose option:
class PostsController < ApplicationController
...
def show
respond_to do |format|
format.html
format.json { render jsonapi: #post, include: :images, expose: {attachment_type: "images"} }
end
end
end
ActiveSupport gives you the post.images_attachments method for free:
class SerializablePost < JSONAPI::Serializable::Resource
type 'posts'
...
has_many :images do
#object.images_attachments
end
end
I created an attachment serializer:
class SerializableAttachment < JSONAPI::Serializable::Resource
include Rails.application.routes.url_helpers
type do
#attachment_type
end
attribute :id
attribute :url do
url_for(#object)
end
end
I needed to tell jsonapi to use this serializer for all attachments:
class ApplicationController < ActionController::Base
...
def jsonapi_class
super.merge(
'ActiveStorage::Attachment': SerializableAttachment
)
end
end
Now I can implement DELETE /attachments/:id.

How to get url of Active Storage image

I want to get list of records with attached images as a links or files by api.
I have a simple model:
class Category < ApplicationRecord
has_one_attached :image
validates :name, presence: true, uniqueness: true
end
And next action:
def index
#categories = Category.all.with_attached_image
render json: #categories.to_json(include: { image_attachment: { include: :blob } })
end
That's the only way I can get image object.
And I see next results:
{"id":4,"name":"Cat1","description":""},
{"id":1,"name":"Cat2","description":"","image_attachment":
{"id":8,"name":"image","record_type":"Category","record_id":1,"blob_id":8,"created_at":"2018-06-09T13:45:40.512Z","blob":
{"id":8,"key":"3upLhH4vGxZEhhf3TaAjDiCW","filename":"Screen Shot 2018-06-09 at 20.43.24.png","content_type":"image/png","metadata":
{"identified":true,"width":424,"height":361,"analyzed":true},"byte_size":337347,"checksum":"Y58zbYUVOlZRadx81wxOJA==","created_at":"2018-06-09T13:45:40.482Z"}}},
...
I can see filename here. But files lives in different folders and it doesn't seems for me like a convenient way to get and link to the file.
I couldn't find any information about this.
Updated
Accordin to iGian solution my code become:
def index
#categories = Category.all.with_attached_image
render json: #categories.map { |category|
category.as_json.merge({ image: url_for(category.image) })
}
end
For my User which has_one_attached :avatar I can get the url in my views with <%= image_tag url_for(user.avatar) %>.
So, in controller I would use just url_for(user.avatar)
For Category which has_one_attached :image:
url_for(category.image)
Also try #object.image.service_url. This will give you the url where the image is saved. I.E. url to amazon s3 storage.
Please follow this for fetching images( in case if you are using has_many_attached)
Model.images.map{|img| ({ image: url_for(img) })}
This works for me with multiple images
class PostSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :content , :images
def images
images = object.images.map do |image|
rails_blob_path(image , only_path: true) if object.images.attached?
end
end
end
I got it to work with rails_blob_url(#object.image). Notice I am calling _url not _path with the helper.
If you want get this url in front end, try this :
<%= url_for(category.image) %>
and for displaying image :
<%= image_tag url_for(category.image) %>
Here is how I use active storage with AWS S3 bucket and attach image urls with domain to JSON response:
activerecord
class Imageable < ApplicationRecord
has_many_attached :images
def image_urls
images.map(&:service_url)
end
def attributes
super.merge({
image_urls: image_urls
})
end
end
application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_active_storage_current_host
def set_active_storage_current_host
ActiveStorage::Current.host = request.base_url
end
end
imageables_controller.rb
class ImageablesController < ApplicationController
include ImageablesHelper
def update
imageable = find_or_create
imageable.update imageables_params
imageable.images.purge
imageable.images.attach imageable_params[:images]
imageable.save!
render json: imageable
end
end
imageables_helper.rb
module ImageablesHelper
def imageables_params
params.require(:imageable).permit(:other_attributes, images: [])
end
end

Save external Tweets in database in Rails

I am new to rails developement and to the MVC architecture. I have a little application where I can add Videos' URLs from Dailymotion or Youtube and get the tweets related to that URL using the twitter gem in Ruby on Rails.
Now i'm able to store the tweets like this : (This is the video controller)
def show
#video = Video.find(params[:id])
# Creating a URL variable
url = #video.url
# Search tweets for the given video/url
#search = get_client.search("#{#video.url} -rt")
# Save tweets in database
#search.collect do |t|
tweet = Tweet.create do |u|
u.from_user = t.user.screen_name.to_s
u.from_user_id_str = t.id.to_s
u.profile_image_url = t.user.profile_image_url.to_s
u.text = t.text.to_s
u.twitter_created_at = t.created_at.to_s
end
end
I'm not sure if this is the right way to do it (doing it in the controller ?), and what I want to do now is to specify that those tweets that have just been stored belong to the current video. Also I would like to have some sort of validation that makes the controller look in the database before doing this to only save the new tweets. Can someone help me with that ?
My models :
class Video < ActiveRecord::Base
attr_accessible :url
has_many :tweets
end
class Tweet < ActiveRecord::Base
belongs_to :video
end
My routes.rb
resources :videos do
resources :tweets
end
This is an example of a "fat controller", an antipattern in any MVC architecture (here's a good read on the topic).
Have you considered introducing a few new objects to encapsulate this behavior? For example, I might do something like this:
# app/models/twitter_search.rb
class TwitterSearch
def initialize(url)
#url = url
end
def results
get_client.search("#{#url} -rt")
end
end
# app/models/twitter_persistence.rb
class TwitterPersistence
def self.persist(results)
results.map do |result|
self.new(result).persist
end
end
def initialize(result)
#result = result
end
def persist
Tweet.find_or_create_by(remote_id: id) do |tweet|
tweet.from_user = screen_name
tweet.from_user_id_str = from_user_id
tweet.profile_image_url = profile_image_url
tweet.text = text
tweet.twitter_created_at = created_at
end
end
private
attr_reader :result
delegate :screen_name, :profile_image_url, to: :user
delegate :id, :user, :from_user_id, :text, :created_at, to: :result
end
Notice the use of find_or_create_by ... Twitter results should have a unique identifier that you can use to guarantee that you don't create duplicates. This means you'll need a remote_id or something on your tweets table, and of course I just guessed at the attribute name (id) that the service you're using will return.
Then, in your controller:
# app/controllers/videos_controller.rb
class VideosController < ApplicationController
def show
#tweets = TwitterPersistence.persist(search.results)
end
private
def search
#search ||= TwitterSearch.new(video.url)
end
def video
#video ||= Video.find(params[:id])
end
end
Also note that I've removed calls to to_s ... ActiveRecord should automatically convert attributes to the correct types before saving them to the database.
Hope this helps!

Ruby on Rails - Paperclip and dynamic parameters

I'm writing some image upload code for Ruby on Rails with Paperclip, and I've got a working solution but it's very hacky so I'd really appreciate advice on how to better implement it. I have an 'Asset' class containing information about the uploaded images including the Paperclip attachment, and a 'Generator' class that encapsulates sizing information. Each 'Project' has multiple assets and generators; all Assets should be resized according to the sizes specified by each generator; each Project therefore has a certain set of sizes that all of its assets should have.
Generator model:
class Generator < ActiveRecord::Base
attr_accessible :height, :width
belongs_to :project
def sym
"#{self.width}x#{self.height}".to_sym
end
end
Asset model:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
# this is utterly horrendous
def styles
s = {}
if #generators == nil
#generators = self.project.generators
end
#generators.each do |g|
s[g.sym] = "#{g.width}x#{g.height}"
end
s
end
end
Asset controller create method:
def create
#project = Project.find(params[:project_id])
#asset = Asset.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.project = #project
#asset.uploaded_by = current_user
respond_to do |format|
if #asset.save_(current_user)
#project.last_asset = #asset
#project.save
format.html { redirect_to project_asset_url(#asset.project, #asset), notice: 'Asset was successfully created.' }
format.json { render json: #asset, status: :created, location: #asset }
else
format.html { render action: "new" }
format.json { render json: #asset.errors, status: :unprocessable_entity }
end
end
end
The problem I am having is a chicken-egg issue: the newly created Asset doesn't know which generators (size specifications) to use until after it's been instantiated properly. I tried using #project.assets.build, but then the Paperclip code is still executed before the Asset gets its project association set and nils out on me.
The 'if #generators == nil' hack is so the update method will work without further hacking in the controller.
All in all it feels pretty bad. Can anyone suggest how to write this in a more sensible way, or even an approach to take for this kind of thing?
Thanks in advance! :)
I ran into the same Paperclip chicken/egg issue on a project trying to use dynamic styles based on the associated model with a polymorphic relationship. I've adapted my solution to your existing code. An explanation follows:
class Asset < ActiveRecord::Base
attr_accessible :image, :deferred_image
attr_writer :deferred_image
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
after_save :assign_deferred_image
def styles
project.generators.each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
private
def assign_deferred_image
if #deferred_image
self.image = #deferred_image
#deferred_image = nil
save!
end
end
end
Basically, to get around the issue of Paperclip trying to retrieve the dynamic styles before the project relation information has been propagated, you can assign all of the image attributes to a non-Paperclip attribute (in this instance, I have name it deferred_image). The after_save hook assigns the value of #deferred_image to self.image, which kicks off all the Paperclip jazz.
Your controller becomes:
# 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
And the view:
<%= form_for #asset do |f| %>
<%# other asset attributes %>
<%= f.label :deferred_upload %>
<%= f.file_field :deferred_upload %>
<%= f.submit %>
<% end %>
This solution also allows using accepts_nested_attributes for the assets relation in the Project model (which is currently how I'm using it - to upload assets as part of creating/editing a Project).
There are some downsides to this approach (ex. validating the Paperclip image in relation to the validity of the Asset instance gets tricky), but it's the best I could come up with short of monkey patching Paperclip to somehow defer execution of the style method until after the association information had been populated.
I'll be keeping an eye on this question to see if anyone has a better solution to this problem!
At the very least, if you choose to keep using your same solution, you can make the following stylistic improvement to your Asset#styles method:
def styles
(#generators || project.generators).each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
Does the exact same thing as your existing method, but more succinctly.
While I really like Cade's solution, just a suggestion. It seems like the 'styles' belong to a project...so why aren't you calculating the generators there?
For example:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.project.styles }
end
class Project < ActiveRecord::Base
....
def styles
#generators ||= self.generators.inject {} do |hash, g|
hash[g.sym] = "#{g.width}x#{g.height}"
end
end
end
EDIT: Try changing your controller to (assuming the project has many assets):
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.uploaded_by = current_user
end
I've just solved a similar problem that I had.
In my "styles" lambda I am returning a different style depending on the value of a "category" attribute. The problem though is that Image.new(attrs), and image.update_attributes(attrs) doesn't set the attributes in a predictable order, and thus I can't be guaranteed that image.category will have a value before my styles lambda is called. My solution was to override attributes=() in my Image model as follows:
class Image
...
has_attached_file :image, :styles => my_lambda, ...
...
def attributes=(new_attributes, guard_protected_attributes = true)
return unless new_attributes.is_a?(Hash)
if new_attributes.key?("image")
only_attached_file = {
"image" => new_attributes["image"]
}
without_attached_file = new_attributes
without_attached_file.delete("image")
# set the non-paperclip attributes first
super(without_attached_file, guard_protected_attributes)
# set the paperclip attribute(s) after
super(only_attached_file, guard_protected_attributes)
else
super(new_attributes, guard_protected_attributes)
end
end
...
end
This ensures that the paperclip attribute is set after the other attributes and can thus use them in a :style lambda.
It clearly won't help in situations where the paperclip attribute is "manually" set. However in those circumstances you can help yourself by specifying a sensible order. In my case I could write:
image = Image.new
image.category = "some category"
image.image = File.open("/somefile") # styles lambda can use the "category" attribute
image.save!
(Paperclip 2.7.4, rails 3, ruby 1.8.7)

Resources