Ember: how to create a join table model using Rails API - ember-data

I'm using Rails5 API and Ember 3.
On the Rails side the model I'd like to save is defined as follows:
class ShopLanguage < ApplicationRecord
belongs_to :shop
belongs_to :language
end
Here is the Shop model:
class Shop < ApplicationRecord
has_many :shop_languages, inverse_of: :shop, dependent: :destroy
has_many :languages, through: :shop_languages
end
Here is the ShopLanguage serializer:
class ShopLanguageSerializer < ActiveModel::Serializer
attributes :id, :shop_id, :language_id, :modified_by
belongs_to :shop
belongs_to :language
end
The problem is that when I'm trying to create a new shop_languagefor a specified shop from Ember:
# controllers/shop-languages.js
actions: {
saveLanguage(aLanguage) {
let shopLanguage = this.store.createRecord('shop-language', {
language: aLanguage,
shop: this.get('currentShop.shop'),
modifiedBy: this.get('currentUser.user').get('username')
});
shopLanguage.save().then(function() {
this.get('flashMessages').info('New language added with success');
});
the language_id is not passed in in params hash in the ShopLanguagesController on the Rails side:
# shop_langauges_controller.rb
class ShopLanguagesController < ApplicationController
before_action :find_shop
before_action :find_language, only: [:create, :destroy]
def create
#shop.languages << #language
json_response(#shop.languages, :created)
end
private
def language_params
params.permit(:language_id, :shop_id, :id, :modified_by)
end
def find_shop
#shop = Shop.find(params[:shop_id])
end
def find_language
#language = Language.find_by!(id: params[:language_id])
end
end
When I check the URL hit by Ember app, it seems to be OK:
POST http://localhost:4200/shops/613/languages
The error comes from find_language method because language_id is null.
Why so ? What is wrong with that ? Thank you

Related

Rails eager loading multiple tables and eager loading from separate tables. How to fix this N+1?

This one is a bit confusing.
I think the line that is problematic is in the controller and it's this line in particular:
recipe_tools = (recipe.recipe_tools + RecipeTool.generic)
My models:
class Recipe < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy
...
end
class RecipeTool < ActiveRecord::Base
belongs_to :story
end
class Story < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy
..
end
This is my controller:
module Api
module Recipes
class RecipeToolsController < Api::BaseController
before_filter :set_cache_buster
def index
# expires_in 30.minutes, public: true
recipe = Recipe.find(params[:recipe_id])
recipe_tools = (recipe.recipe_tools + RecipeTool.generic)
binding.pry
render json: recipe_tools, each_serializer: Api::V20150315::RecipeToolSerializer
end
end
end
end
This is my serializer:
module Api
module V20150315
class RecipeToolSerializer < ActiveModel::Serializer
cached
delegate :cache_key, to: :object
attributes :id,
:display_name,
:images,
:display_price,
:description,
:main_image,
:subtitle
def display_name
object.display_name
end
def images
object.story.get_spree_product.master.images
end
def display_price
object.story.get_spree_product.master.display_price
end
def description
object.story.description
end
def main_image
object.story.main_image
end
def subtitle
object.story.get_spree_product.subtitle
end
def spree_product
binding.pry
spree_product.nil? ? nil : spree_product.to_hash
end
private
def recipe_tool_spree_product
#spree_product ||= object.story.get_spree_product
end
end
end
end
This is my RecipeTool model:
class RecipeTool < ActiveRecord::Base
...
scope :generic, -> { where(generic: true) }
end
In the controller, we call recipe.recipe_tool only once and so I don't think we need to includes recipe_tool. We're not iterating through a collection of recipes and calling recipe_tool on each one so no N+1 problem.
However, we are creating a collection of recipe_tools in the controller by concatenating two collections of recipe_tools together. Recipe.generic is also a SQL query that generates generic recipe_tools.
I think the N+1 problem is happening in generating the JSON response via the serializer. We call recipe_tool.story a lot which would generate a SQL queries each time we call #story and we do that on a collection of recipe_tools.
First, I would fix your associations using :inverse_of, so that I wouldn't have to worry about rails reloading the objects if it happened to traverse back up to a parent object. ie
class Recipe < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy, :inverse_of=>:recipe
...
end
class RecipeTool < ActiveRecord::Base
belongs_to :story, :inverse_of => :recipe_tools
belongs_to :recipe, :inverse_of => :recipe_tools ## this one was missing???
end
class Story < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy, :inverse_of=>:story
..
end
Next I would eager_load the appropriate associations in the controller, something like:
ActiveRecord::Associations::Preloader.new.preload(recipe_tools, :story =>:recipe_tools, :recipe=>:recipe_tools)
before calling the serializer.

Rails updating model through association

I have the following two models:
class Process < ActiveRecord::Base
has_many :activities, inverse_of: :artifact, dependent: :destroy
attr_accessible :name, :activities_attributes
def update_status!
if self.activities.all? {|a| a.completed? }
self.status = 'completed'
elsif self.activities.any? {|a| a.completed? }
self.status = 'in_progress'
else
self.status = 'not_started'
end
save!
end
end
class Activity < ActiveRecord::Base
belongs_to :process, inverse_of: :activities
attr_accessible :name,:completed_date
scope :completed, where("completed_date is not null")
end
Then in my Controller:
#activity = Activity.find(params[:id])
#activity.completed_date = Time.now
#activity.process.update_status!
If I put a debugger directly after this line, and print out #activity.completed it returns true, however #artifact.status is still "not_started" (assume no other activities).
However, if I add the following line before the update:
#activity.process.activities[#activity.process.activities.index(#activity)] = #activity
The status is updated correctly.
Why doesn't the change to #activity propagate into process.activities? And how can I make it propagate?
I don't this inverse_of works with has_many through. See this article: ActiveRecord :inverse_of does not work on has_many :through on the join model on create
Here is the relevant blurb from the RailsGuides:
There are a few limitations to inverse_of support:
They do not work with :through associations. They do not work with
:polymorphic associations. They do not work with :as associations. For
belongs_to associations, has_many inverse associations are ignored.

How to forbid deletion if association present

I have a many to many relationship between two models as follows:
#users.rb
has_many :users_to_roles
has_many :roles, through: :users_to_roles
#users_to_roles.rb
belongs_to :user
belongs_to :role
#roles.rb
has_many :users_to_roles
has_many :users, through: :users_to_roles
I want to disable the deletion of roles if there are users who are "in this role". Here I have found two options who should do the work:
:restrict_with_exception causes an exception to be raised if there are
any associated records :restrict_with_error causes an error to be
added to the owner if there are any associated objects
but there is no example with the syntax of this and how it should work.
Could you help to make this valid:
#roles.rb
has_many :users_to_roles
has_many :users, through: :users_to_roles, dependent: restrict_with_exception
Such operations can be easily do using Callbacks. In my case, I have added the following method in my model:
# callbacks
before_destroy :check_for_users_in_this_role
def check_for_users_in_this_role
status = true
if self.security_users.count > 0
self.errors[:deletion_status] = 'Cannot delete security role with active users in it.'
status = false
else
self.errors[:deletion_status] = 'OK.'
end
status
end
Alternatively, you can rescue the exception in your controller. In this example, a contact may own interest, i.e.
class Interest < ActiveRecord::Base
belongs_to :contact
end
class Contact < ActiveRecord::Base
has_many :interests, :dependent => :restrict
end
Then in the controller:
def destroy
#contact = Contact.find(params[:id])
begin
#contact.destroy
rescue
flash[:msg] = "Can't delete - owns interest"
end
respond_to do |format|
format.html { redirect_to(:back) }
format.xml { head :ok }
end
end
The flash message will be displayed in the calling page.
The correct rails way is to do the following:
users.rb:
has_many :users_to_roles, dependant: :destroy # don't keep the join table entry if the user is gone
has_many :roles, through: :users_to_roles
Make sure that your join does not have redundant entries (in which either column is null or orphaned).
users_to_roles.rb:
belongs_to :user
belongs_to :role
# add validations presence of both user and role
# in both model and database.
Bonus, from rails 4.2 you can add forigen_key: true in your migration for referential integrity
Now in your role (I am assuming you name your models singularly and made a typo in the question), you add this:
role.rb:
has_many :users_to_roles, dependant: :restrict_with_error
has_many :users, through: :users_to_roles
I made it with my classes like this:
app/models/guest_chat_token.rb
class GuestChatToken < ApplicationRecord
has_many :chat_messages, as: :sendable, dependent: :restrict_with_exception
end
app/controllers/admin/application_controller.rb
class Admin::ApplicationController < ApplicationController
....
rescue_from ActiveRecord::DeleteRestrictionError do |exception|
redirect_to :back, notice:
"Be aware: #{exception.message}."
end
end

Validate multiple belongs_to relations

Let's say we have the following data structure (imagine as a loop).
MyModel * -- 1 User 1 -- * Clients 0 -- * MyModel
Thus, the MyModel looks like:
class MyModel < ActiveRecord::Base
belongs_to :client
belongs_to :user
attr_accessible :user_id, :client_id, # ...
validates :user_id, :presence => true
So it can belong to a client but must belong to a user. Also a client must belong to a user.
But how can I assert, that if someone saves a MyModel instance belonging to a client, that the client actually belongs to the user? Do I have to write a custom validator for this or is there any validation shortcut I overlooked?
Solution:
Following p.matsinopoulos' answer, I now did the following. I don't assign the foreign keys to MyModel before saving but rather the objects directly.
# my_model_controller.rb
def create
m = MyModel.create(whitelist_parameters(params[:my_model]))
m.user = current_user
if(params[:my_model][:client_id].present?)
client = Client.find params[:my_model][:client_id]
end
# ...
end
This way I can at first validate if there is a Client with such an ID at all. Then I implemented the custom validator by p.matsinopoulos. It's not tested yet, but since I am now working with the objects, it should do the trick.
If I were you, I would have written a custom validator.
validate :client_belongs_to_user
protected
def client_belongs_to_user
errors[:client] << 'must own the user' if client.present? && client.user != user
end
I would suggest, building on what you have:
class MyModel < ActiveRecord::Base
belongs_to :client
has_one :user, :through => :client
attribute_accessor :client_id
validates :client_id, :presence => true
...
end
class Client < ActiveRecord::Base
has_many :my_models
belongs_to :user
attribute_accessor :user_id
validates :user_id, :presence => true
...
end
class User < ActiveRecord::Base
has_many :clients
...
end

Added two "belongs_to" to a Comment model but unable to get one of the associations

I am currently building very simple Comment system on Rails. The primary models are User, Albumpost, and Comment. Users can post Albumposts. For each Albumpost, Users can add Comments to the Albumpost. As a result, a Comment belongs to a User and belongs to an Albumpost.
The problem I'm having is that even with the proper associations in my models (see below), I can't get
#comment.user.name
when I'm trying to render the comments in the albumpost 'show' page (/views/albumposts/show.html.erb). When I go to the page, I can't get #comment.user.name (doesn't understand the association) and get a
"undefined method `name' for nil:NilClass"
Oddly I can get
#comment.albumpost.content
I've double-checked my models and also added the proper foreign keys to the models. Am I doing something wrong in the controllers?
Here are my models:
class Comment < ActiveRecord::Base
attr_accessible :body, :albumpost_id, :user_id
belongs_to :albumpost
belongs_to :user
end
class Albumpost < ActiveRecord::Base
attr_accessible :content
belongs_to :user
has_many :comments, dependent: :destroy
end
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_many :albumposts, dependent: :destroy
has_many :comments, dependent: :destroy
end
Here are the relevant parts of my Albumpost and Comments controllers:
class AlbumpostsController < ApplicationController
def show
#albumpost = Albumpost.find(params[:id])
#comments = #albumpost.comments
#comment = Comment.new
#comment.albumpost_id = #albumpost.id
#comment.user_id = current_user.id
end
end
class CommentsController < ApplicationController
def create
albumpost_id = params[:comment].delete(:albumpost_id)
#comment = Comment.new(params[:comment])
#comment.albumpost_id = albumpost_id
#comment.user_id = current_user.id
#comment.save
redirect_to albumpost_path(#comment.albumpost)
end
end
I think you should prefer setting objects to relations instead of setting their ids. For example, you should do this:
#comment.user = current_user
instead of
#comment.user_id = current_user.id
ActiveRecord will take care of setting corresponding *_id fields. I'm not sure how it handles the reverse. (it should autoload though, if I understand correctly)