Ruby on Rails joins models for json output - json

I'm trying to build a JSON API end point with Ruby on Rails.
I followed the instruction in the following and was able to create JSON API for my models
http://railscasts.com/episodes/350-rest-api-versioning?view=comments/
I have the following controller:
/api/v1/movies_controller.rb
class MoviesController < ApplicationController
def index
if params[:p].nil?
p = 1
else
p = params[:p].to_i
end
#movies = Movie.order("id DESC").page(p)
end
def show
#movie = Movie.find(params[:id])
end
end
I need to join this with the Genre object where Movie has_many :genres, and Genre belongs_to :movie
However, I'm not able to use the following to get the genres joined with the movie object for the JSON output:
#movie = Movie.find(params[:id], :joins => :genres)
I did notice that the following command is able to generate the joined output in ruby console
#movie.to_json(:include=>:genres)
But then adding this in the controller doesn't show the additional genres fields
Can someone help me please?
Thanks!

My advise: Go with active_model_serializers
Add the following line to your Gemfile
gem 'active_model_serializers'
Do a bundle install afterwards.
After that do a
rails g serializer movie
rails g serializer genre
Than customize the following code:
/api/v1/movies_controller.rb
class MoviesController < ApplicationController
respond_to :json
def index
if params[:p].nil?
p = 1
else
p = params[:p].to_i
end
#movies = Movie.order("id DESC").page(p)
respond_with #movies
end
def show
#movie = Movie.find(params[:id])
respond_with #movie
end
end
app/serializers/movie_serializer.rb
class MovieSerializer < ActiveModel::Serializer
embed :ids, :include => true
attributes :id, :name
has_many :genres
end
app/serializers/genre_serializer.rb
class GenreSerializer < ActiveModel::Serializer
embed :ids, :include => true
attributes :id, :name
end
Have a look at the full documentation of active_model_serializers at
https://github.com/rails-api/active_model_serializers

Related

ActiveRecord::AssociationTypeMismatch Rails CSV Import

I am using gem roo to import CSV data. It works smoothly, until the point where there is an association, and am hoping that roo can translate the string into the corresponding integer value in the association. In my case, I have a Staff model which belongs to State.
class State < ApplicationRecord
has_many :staffs
end
class Staff < ApplicationRecord
belongs_to :state
end
This means that I have state_id column in the staffs table. In my CSV, however, the end user has the names of the states, which correspond to the ones in the states tables. When I try to import the CSV, I get the error:
ActiveRecord::AssociationTypeMismatch in StaffsImportsController#create
State(#134576500) expected, got "Texas" which is an instance of String(#20512180)
The highlighted source is:
staff.attributes = row.to_hash
Is it possible for gem roo to translate 'Texas' in the csv file to, say, id 2, instead of the end user doing a lot of translation work before uploading the data?
Here is staffs_imports.rb
class StaffsImport
include ActiveModel::Model
require 'roo'
attr_accessor :file
def initialize(attributes={})
attributes.each { |name, value| send("#{name}=", value) }
end
def persisted?
false
end
def open_spreadsheet
case File.extname(file.original_filename)
when ".csv" then Csv.new(file.path, nil, :ignore)
when ".xls" then Roo::Excel.new(file.path, nil, :ignore)
when ".xlsx" then Roo::Excelx.new(file.path)
else raise "Unknown file type: #{file.original_filename}"
end
end
def load_imported_staffs
spreadsheet = open_spreadsheet
header = spreadsheet.row(1)
(2..spreadsheet.last_row).map do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
staff = Staff.find_by_national_id(row["national_id"]) || Staff.new
staff.attributes = row.to_hash
staff
end
end
def imported_staffs
#imported_staffs ||= load_imported_staffs
end
def save
if imported_staffs.map(&:valid?).all?
imported_staffs.each(&:save!)
true
else
imported_staffs.each_with_index do |staff, index|
staff.errors.full_messages.each do |msg|
errors.add :base, "Row #{index + 6}: #{msg}"
end
end
false
end
end
end
And finally the staff_imports_controller.rb:
class StaffsImportsController < ApplicationController
def new
#staffs_import = StaffsImport.new
end
def create
#staffs_import = StaffsImport.new(params[:staffs_import])
if #staffs_import.save
flash[:success] = "You have successfully uploaded your staff!"
redirect_to staffs_path
else
render :new
end
end
end
Any help/clues will be highly appreciated.
I managed to get a solution to this, thanks to a wonderfully detailed question and great answer provided here Importing CSV data into Rails app, using something other then the association "id"

Delete an element from a collection

I'm facing to a stupid problem. I have created a collection select which is creating elements into a join table "staffs_task" to reference an association between the model staff and task.
And now I would like two things: (1) a button delete this association (2) and a little bit of code for my model staffs_task to avoid duplication, so with the task_id and staff_id. And last info, task is a model built by ranch
my code:
(the collection in new_task)
<%= select_tag "staffs_task", options_from_collection_for_select(#staffs, 'id', 'name') , :multiple => true %>
(task_controller)
skip_before_action :configure_sign_up_params
before_action :set_ranch
before_action :set_task, except: [:create]
def create
#task = #ranch.tasks.create(task_params)
#staffs = Staff.where(:id => params[:staffs_task])
#task.staffs << #staffs
if #task.save
#task.update(done: false)
#task.update(star: false)
flash[:success] = "The task was created "
else
flash[:success] = "The task was not created "
end
redirect_to #ranch
end
private
def task_params
params.require(:task).permit(:content, :deadline, :row_order, :date, :assigned_to)
end
def set_ranch
#ranch = Ranch.find(params[:ranch_id])
end
def set_task
#task = #ranch.tasks.find(params[:id])
end
So if you have any idea about one of this two things, your help would be welcome
Thanks in advance !!
Lets say you have the following many to many setup with a join model:
class Staff
has_many :assignments
has_many :tasks, through: :assignments
end
class Task
has_many :assignments
has_many :staff, through: :assignments
end
class Assignment
belongs_to :task
belongs_to :staff
end
Note that the plural of staff is staff - unless you are talking about the sticks carried by wizards.
ActiveRecord creates "magical" _ids setters for all has_many relationships. When used with a has_many through: relationship rails is smart enough to just remove the rows from the join table.
You can use this with the collection_select and collection_checkboxes methods:
<%= form_for([#task.ranch, #task]) do |f| %>
<%= f.collection_select(:staff_ids, Staff.all, :id, :name, multiple: true) %>
<% end %>
You would then set your controller up like so:
def create
#task = #ranch.tasks.new(task_params) do |t|
# this should really be done by setting default values
# for the DB columns
t.done = false
t.star = false
end
if #task.save
redirect_to #ranch, success: "The task was created"
else
render :new, error: "The task was not created"
end
end
private
def task_params
params.require(:task)
.permit(:content, :deadline, :row_order, :date, :assigned_to, staff_ids: [])
end
staff_ids: [] will allow an array of scalar values. Also not that .new and .create are not the same thing! You where saving the record 4 times if it was valid so the user has to wait for 4 expensive write queries when one will do.

How do I add checkbox filters to my Posts index page (Rails app)

I have a Rails app that has many Posts. Each Post has different parameters, two of these parameters are zagat_status and michelin_status. (it's a restaurant discovery website).
I'm trying to add 2 checkboxes that allow me to filter results for:
a) zagat_status .. so clicking the checkbox shows all Posts where zagat_status is "Yes"
b) michelin_status .. so clicking the checkbox shows all Posts where michelin_status = "1", "2", or "3"
Moreover, I want these two checkboxes to be able to work with each other. So if I click both, both filters apply simulatneously.
However, this is not working... how do I get the code below to work??
POST MODEL
class Post < ActiveRecord::Base
scope :zagat_status, -> (zagat_status) { where zagat_status: zagat_status }
scope :michelin_status, -> (michelin_status) { where michelin_status: michelin_status }
validates :name, presence: true
validates :city, presence: true
validates :address, presence: true
def self.search(query)
where("name like ? OR city like ? OR address like ?", "%#{query}%", "%#{query}%", "%#{query}%")
end
end
POST CONTROLLER
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
# GET /posts
# GET /posts.json
def index
#posts = Post.all
if params[:search]
#posts = Post.search(params[:search]).order("created_at DESC")
elsif params[:zagat_status].present?
#posts = Post.zagat_status(params[:zagat_status]).order("created_at DESC")
elsif params[:michelin_status].present?
#posts = Post.michelin_status(params[:michelin_status]).order("created_at DESC")
else
#posts = Post.all
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
#post = Post.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def post_params
params.require(:post).permit(
:name,
:michelin_status,
:zagat_status,
:address,
:city,
:cuisine,
:neighborhood,
:price_range,
:longitude,
:latitude)
end
end
INDEX.HTML.ERB FILE (for POSTS)
<div class="search-filter">
<form>
<span>Accolades</span>
<div class="accolades-panel">
<label>
Michelin Star(s) <dd>1,2,3 and bibs</dd>
<input type="checkbox" name="michelin_status" value="1">
</label>
<label>
Zagat<dd>rated</dd><input type="checkbox" name="zagat_status" value="Yes">
</label>
</div>
</form>
</div>
INDEX.JS.ERB FILE
Blank
INDEX.JSON.JBUILDER FILE
json.array!(#posts) do |post|
json.extract! post, :id, :name, :zagat_status, :michelin_status, :cuisine, :address, :city, :price_range, :longitude, :latitude
json.url post_url(post, format: :json)
end
Your code doesn't work because the results are returned immediately after one of the conditions in the controller are satisfied e.g. if params[:search] exists it will return:
#posts = Post.search(params[:search]).order("created_at DESC")
And the rest of the code won't be run, because it's inside elsif blocks.
Something like this should work:
#posts = Post.all
if params[:search]
#posts = #posts.search(params[:search]).order("created_at DESC")
end
if params[:zagat_status].present?
#posts = #posts.zagat_status(params[:zagat_status]).order("created_at DESC")
end
if params[:michelin_status].present?
#posts = #posts.michelin_status(params[:michelin_status]).order("created_at DESC")
end
This way we build the query based on the existence of parameters. However this is not the most elegant solution.
I would suggest you create a method in your model that would take in the parameters, and then create a query there based on the parameters.

Mongoid embedded documents and Rails strong parameters not working

I have a 1-N relationship in Mongoid/Rails:
class Company
include Mongoid::Document
field :name, type: String
embeds_many :people, class_name: 'Person'
end
class Person
include Mongoid::Document
field :first_name, type: String
embedded_in :company, class_name: 'Company', inverse_of: 'people'
end
Now I can successfully create a Company as follows in the console; for example:
> c = Company.new(name: 'GLG', :people => [{first_name: 'Jake'}]) # OK!
> c.people # OK!
Then I have a JSON API controller to update a company, along the lines of:
# PUT /api/companies/:id
def update
if Company.update(company_params)
# ... render JSON
else
# ... render error
end
end
private
def company_params
params.require(:company).permit(:name, :people => [:first_name])
end
Now, when the PUT request comes in from the frontend, the company_params is always missing the :people attribute. Rails log says:
Parameters: {"id"=>"5436fbc64a616b5240050000", "name"=>"GLG", "people"=>[{"first_name"=>"Jake"}], "company"=>{"name"=>"GLG"}}
I don't get an "Unpermitted parameters" warning. I've tried every conceivable way of permitting the people field and it still doesn't get included.
params.require(:company).permit!
Results in the same. What am I doing wrong?
You have to accept nested_attributes on assignment
class Company
include Mongoid::Document
field :name, type: String
embeds_many :people, class_name: 'Person'
accepts_nested_attributes_for :people
end

How can I save a unique string by incrementing it in rails?

How can I save a unique string to my database and if the value exists increment it.
The behaviour I'm after is similar to when you save a file e.g. foo.txt, foo1.txt
I do NOT want to return a 'not unique value' error message.
class Person < ActiveRecord::Base
attr_accessible :name
end
Person.create(:name => 'Dave') # => 'Dave'
Person.create(:name => 'Dave') # => 'Dave1'
Person.create(:name => 'Dave') # => 'Dave2'
I'm using ruby, rails and mysql
you don't need to set validation uniqueness to your model . You can use something like this:
class Person < ActiveRecord::Base
attr_accessible :name
before_create :check_and_increment
private
def check_and_increment
if Person.exists? name: self.name
similar_persons = get_all_with_same_name self.name
next_num = similar_persons.max_by{|m| m.scan(/\d+/)}.scan(/\d+/).first.nil? ? 1 : similar_persons.max_by{|m| m.scan(/\d+/)}.scan(/\d+/).first.to_i + 1
self.name = "#{self.name}#{next_num}"
else
true
end
end
def get_all_with_same_name name
where("name LIKE ?", 'name%')
end
end
This is simple idea for your problem. Need to be carefull with persons like Anna and Annastasia, for example . These persons has different names but their names are overlap.
Hope this will help you .
I could not find any rails or mysql features to do this so I created the follow 3 methods and
def unique_name(name)
if Person.exists?(:name => name)
generate_unique_name(name)
else
name
end
end
def similar_names(name)
Person.where("name LIKE ?", "#{name}%").pluck(:name)
end
def generate_unique_name(name)
number = similar_names(name).map{ |a| a.scan(/\d+$/).max.to_i }.compact.max.to_i + 1
"#{name} #{number}"
end
...
Person.create(:name => unique_name('Dave'))