Rails - How get first record of associated table? - ruby-on-rails

i am trying to create an api for my mobile app.
I have posts and images tables. For my api, i can send all posts with:
#posts = Post.all
render json: #posts
Output: [{"id":20,"title":"Title 1", "body":" first post ", "user_id":1 }]
But it does not contain images at all. In order to show a showcase image in homepage of my app, i just need the first image of associated images.
The output which i need is (the name of showcase_image attribute does not matter) :
Output: [{"id":20, "title":"Title 1", "body":" first post ", "showcase_image": 'first_image.jpg' , "user_id":1 }]
I need to include first image from associated images table to my json response..
Thanks in advance !

I would suggest using a serializer. Active Model Serializer is pretty standard and easy to use, but is not receiving any updates and has a bad performance. You can choose any other serializer (I recommend Blueprinter) or use the AMS.
Through the AMS you coudl define the relation you want to serialize and it would build the json you're expecting
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :showcase_image, :user_id
def showcase_image
object.images.first.name # don't know what is the attribute you're looking for
end
end
And on your controller:
#posts = Post.includes(:images).all # Use includes to avoid N+1 problems
render json: #posts, serialize_collection: PostSerializer

You can include associations with the :include option when calling as_json.
render json: #posts.as_json(include: :images)
You could limit this to one image by adding a new association to Post.
class Post < ApplicationRecord
has_many :images
has_one :showcase_image, class_name: 'Image'
end
This would allow you to use the :showcase_image instead.
render json: #posts.as_json(include: :showcase_image)
You could also use Jbuilder to solve the issue at hand without adding an additional association.
# app/views/posts/index.json.jbuilder
# Get images that belong to posts, group them by post_id and
# return the minimum image id for each post_id.
images = Images.where(post_id: #posts.select(:id)).group(:post_id).minimum(:id)
# Request the full image data for all image ids returned above.
images = images.keys.zip(Image.find(images.values)).to_h
json.array! #posts do |post|
json.extract! post, :id, :title, :body, :...
json.showcase_image do
image = images[post.id]
if image
json.extract! image, :id, :name, :location, :...
else
json.null!
end
end
end
Without calling a specific render, Rails will default to the app/views/posts/index file, and select the file matching the request. (If you request HTML it will look for an HTML file, if you request JSON it looks for JSON, etc.)
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all
end
end
Now when you request /posts.json or /posts with the header Accept: application/json your application should return the JSON response build by Jbuilder.

Related

How to use CollectionSerializer in FastJson API?

I'm updating the API code to FastJson (https://github.com/Netflix/fast_jsonapi) in where I work. The "old" code is using ActiveModel and has
ActiveModel::Serializer::CollectionSerializer.new. I have no idea how to "translate" this code to FastJson API.
I already searched in FastJson documentation about Collection Serialization (https://github.com/Netflix/fast_jsonapi#collection-serialization), but i didnt understand the example.
class API::Messages::MessagesSerializer < ActiveModel::Serializer
attributes :id, :name, :description
attribute :chats do
ActiveModel::Serializer::CollectionSerializer.new(
object.user_chats, serializer: API:Messages::ChatUserSerializer
)
end
end
When collection will be passed in serializer it'll deal that collection perfectly no need to configure anything extra. Following are the serializers
class API::Messages::MessagesSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name, :description
attributes :chats do |message|
API::Messages::ChatsSerializer.new(message. user_chats)
end
end
class API::Messages::ChatsSerializer
include FastJsonapi::ObjectSerializer
attributes ... # add attribute/logic as you want for single chat object
end
And your controller will be something like this
def show
render json: API::Messages::MessagesSerializer.new(#message).serialized_json, status: :ok
end

Attribute is not rendered in json answer

I'm changing existing serializer in ruby on rails(4.2) project adding new custom attribute to serializer(test123), which is not in model. But this attribute is not rendered in json answer which is formed via this serializer. Also i can change values of existing attributes (like author_name), which is in model.
Model contains of id and author_name.
Json is formed in controller of a class which has_many Examples.
My serializer:
class ExampleSerializer < ActiveModel::Serializer
attributes :id,
:author_name,
:test123
def test123
Rails.logger.debug("???!!!")
"test"
end
def author_name
"test"
end
end
Part of my controller:
def show
render json: #system,
include: %w[examples and other included data],
adapter: :json_api_secured,
each_serializer: detect_serializer
end
Server calls test123 (i see it by print ???!!! in logs), but in formed answer there is no field test123.
What can be a problem?

Embeds_many child property not persisted when saving parent

I've been looking for some days without finding the exact answer to my problem which is as simple as that : I have a simple model, with books and authors. A book embeds many authors, and an author is embedded in book. But whenever I'm saving a new book, the author array is not persisted.
What I have is an angular 7 application, calling a ROR API. My Rails versions is 5.2.2. I am using mongoid 7.0 for persistence.
My API was generated with rails g scaffold, and with the --api and --skip-active-record flags.
I first had a problem with the mapping of my properties. My Angular APP sends JSON in lowerCamelCase, when Rails awaits form lower_snake_case vars. I managed to bypass this problem by adding a middleware (correct me if I'm wrong on this one) in my initializers which converts camelCase to snake_case.
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = -> (raw_post) {
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
data = {:_json => data} unless data.is_a?(Hash)
# Transform camelCase param keys to snake_case:
data.deep_transform_keys!(&:underscore)
}
From what I found looking for my problem, it could have been a problem with strong params, so I tried to get awat with this in my book_params
def book_params
#params.fetch(:book, {})
params.require(:book).permit(:title, :release_date, authors_attributes: [:name, :last_name, :birth_date])
end
These are my model :
class Person
include Mongoid::Document
field :last_name, type: String
field :first_name, type: String
field :birth_date, type: Date
end
class Author < Person
include Mongoid::Document
embedded_in :book
end
class Book
include Mongoid::Document
field :title, type: String
field :release_date, type: Date
embeds_many :authors
accepts_nested_attributes_for :authors
end
And this is POST in my book controller (generated with Rails)
# POST /books
def create
#book = Book.new(book_params)
if #book.save
render json: #book, status: :created, location: #book
else
render json: #book.errors, status: :unprocessable_entity
end
end
And here are exemple of a body sent, received and how it is processed by Rails :
Request sent by angular app
Request received and processed by Rails
We can see in the book object
"book"=>{"title"=>"azerty", "release_date"=>"2019-01-21T16:10:19.515Z"}}
That the authors have disappeared, though they were present in the request received by the server.
My question is then : what is the solution to this, or at least what am I missing ? Doesn't Mongoid automatically save children when using embedded documents and accepts_nested_attributes_for ? Should I manually save the children each time a parent is saved in my controller ?
Thanks in advance for helping me
You have to use nested attributes to save children records
Add following line in book model
accepts_nested_attributes_for :authors
And pass authors parameters in author_attributes, for exa:
{title: 'test', release_date: '', author_attributes: [{first_name: '', other_attributes of author}, {first_name: '', , other_attributes of author}]}
for more details please check Mongoid: Nested attributes
Pass perameters in this format
{"title"=>"test", "release_date"=>"2019-01-22", "book"=>{"title"=>"test", "release_date"=>"2019-01-22", "authors_attributes"=>[{"first_name"=>"test name", "last_name"=>"test", "birth_date"=>"2019-01-22T09:43:39.698Z"}]}}
Permit book params
def book_params
params.require(:book).premit(:first_name, :last_name, authors_attributes: %i[first_name last_name birth_date])
end

How to write activemodel serializer for many to many relationship?

Trying to set up the backend for an ember-cli app. Here's how the models look like in Ember:
post.js
export default DS.Model.extend({
heading: DS.attr('string'),
content: DS.attr(''),
imageUrl: DS.attr('string'),
pageId: DS.belongsTo('page'),
tagIds: DS.hasMany('tag')
});
tag.js
export default DS.Model.extend({
name: DS.attr('string'),
postIds: DS.hasMany('post')
});
The models in Rails and Active Record are just Post, Tag, and Theme. Theme joins Post and Tag. (ie: Post has_many :tags, through: :themes)
Here's what my serializers look like:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :heading, :content, :image_url
has_many :tags
end
class TagSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :name
end
This works in one direction: searching posts will get all the tags as well. Doesn't work in the other because I don't have a has_many in the TagSerializer. However, if I put a has_many in both serializers, there will be a stack level too deep error.
So I guess my question is: What is the typical way to implement a many-to-many relationship with ActiveModel serializer? I can't seem to find any resources on how to set this up in a Rails back end. Any help would be appreciated. Thanks!
You're getting a "stack level too deep" error because each serializer is recursively embedding the other.
I'd start by making sure you're using includes in your Rails controller:
# posts controller
def show
post = Post.includes(:tags).find_by id: params[:id]
render json: post
end
# tags controller
def show
tag = Tag.includes(:posts).find_by id: params[:id]
render json: post
end
Then, in your serializer, tell it to conditionally include tags / posts only if the association has been loaded:
# post serializer
def include_tags?
object.association(:tags).loaded?
end
# tag serializer
def include_posts?
object.association(:posts).loaded?
end
After this, it should only cascade down one level.
As a side note, you'll probably want to rename the tagIds and postIds properties in your ember models to tags and posts.

Update owner tags via form

I would like to uniquely use owner tags in my app. My problem is that when I create / update a post via a form I only have f.text_field :tag_list which only updates the tags for the post but has no owner. If I use f.text_field :all_tags_list it doesn't know the attribute on create / update. I could add in my controller:
User.find(:first).tag( #post, :with => params[:post][:tag_list], :on => :tags )
but then I have duplicate tags, for post and for the owner tags. How can I just work with owner tags?
The answer proposed by customersure (tsdbrown on SO) on https://github.com/mbleigh/acts-as-taggable-on/issues/111 works for me
# In a taggable model:
before_save :set_tag_owner
def set_tag_owner
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(account, :tags, self.tag_list)
# Clear the list so we don't get duplicate taggings
self.tag_list = nil
end
# In the view:
<%= f.text_field :tag_list, :value => #obj.all_tags_list %>
I used an observer to solve this. Something like:
in /app/models/tagging_observer.rb
class TaggingObserver < ActiveRecord::Observer
observe ActsAsTaggableOn::Tagging
def before_save(tagging)
tagging.tagger = tagging.taggable.user if (tagging.taggable.respond_to?(:user) and tagging.tagger != tagging.taggable.user)
end
end
Don't forget to declare your observer in application.rb
config.active_record.observers = :tagging_observer
Late to the party, but I found guillaume06's solution worked well, and I added some additional functionality to it:
What this will enable: You will be able to specify the tag owner by the name of the relationship between the tagged model and the tag owner model.
How: write a module and include in your lib on initialization (require 'lib/path/to/tagger'):
module Giga::Tagger
extend ActiveSupport::Concern
included do
def self.tagger owner
before_save :set_tag_owner
def set_tag_owner
self.tag_types.each do |tag|
tag_type = tag.to_s
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(owner, :"#{tag_type}", self.send(:"#{tag_type.chop}_list"))
# Clear the list so we don't get duplicate taggings
self.send(:"#{tag_type.chop}_list=",nil)
end
end
end
end
end
Usage Instructions:
Given: A model, Post, that is taggable
A model, User, that is the tag owner
A post is owned by the user through a relationship called :owner
Then add to Post.rb:
include Tagger
acts_as_taggable_on :skills, :interests, :tags
tagger :owner
Make sure Post.rb already has called acts_as_taggable_on, and that User.rb has acts_as_tagger
Note: This supports multiple tag contexts, not just tags (eg skills, interests)..
the set_tag_owner before_save worked for me. But as bcb mentioned, I had to add a condition (tag_list_changed?) to prevent the tags from being deleted on update:
def set_tag_owner
if tag_list_changed?
set_owner_tag_list_on(account, :tags, tag_list)
self.tag_list = nil
end
end
When working with ownership the taggable model gets its tags a little different. Without ownership it can get its tags like so:
#photo.tag_list << 'a tag' # adds a tag to the existing list
#photo.tag_list = 'a tag' # sets 'a tag' to be the tag of the #post
However, both of these opperations create taggins, whose tagger_id and tagger_type are nil.
In order to have these fields set, you have to use this method:
#user.tag(#photo, on: :tags, with: 'a tag')
Suppose you add this line to the create/update actions of your PhotosController:
#user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
This will create two taggings (one with and one without tagger_id/_type), because params[:photo][:tag_list] is already included in photo_params. So in order to avoid that, just do not whitelist :tag_list.
For Rails 3 - remove :tag_list from attr_accessible.
For Rails 4 - remove :tag_list from params.require(:photo).permit(:tag_list).
At the end your create action might look like this:
def create
#photo = Photo.new(photo_params) # at this point #photo will not have any tags, because :tag_list is not whitelisted
current_user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
if #photo.save
redirect_to #photo
else
render :new
end
end
Also note that when tagging objects this way you cannot use the usual tag_list method to retrieve the tags of a photo, because it searches for taggings, where tagger_id IS NULL. You have to use instead
#photo.tags_from(#user)
In case your taggable object belongs_to a single user you can also user all_tags_list.
Try using delegation:
class User < ActiveRecord::Base
acts_as_taggable_on
end
class Post < ActiveRecord::Base
delegate :tag_list, :tag_list=, :to => :user
end
So when you save your posts it sets the tag on the user object directly.
I ended up creating a virtual attribute that runs the User.tag statement:
In my thing.rb Model:
attr_accessible :tags
belongs_to :user
acts_as_taggable
def tags
self.all_tags_list
end
def tags=(tags)
user = User.find(self.user_id)
user.tag(self, :with => tags, :on => :tags, :skip_save => true)
end
The only thing you have to do is then change your views and controllers to update the tag_list to tags and make sure you set the user_id of the thing before the tags of the thing.

Resources