If I have the following models in Rails, how would I represent this in Ember/Ember Data?
class Attachment < ActiveRecord::Base
belongs_to :user
belongs_to :attachable, polymorphic: true
end
class Profile < ActiveRecord::Base
belongs_to :user
has_one :photo, class_name: 'Attachment', as: :attachable
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :attachments, as: :attachable
end
References I've found are the relevant ember-data pull request, the ember-data tests for polymorphic relationships, and this related question but it's difficult to work out a canonical example from them.
I now use two different ways of working with rails-style "polymorphic" models. I have updated the code below to show both uses.
Attachment model: This is "polymorphic" on the Rails side but is always "embedded" on the Ember side. The reason for this is that I currently only need to save/update attachments along with their associated model.
Comment model: This is polymorphic on both the Rails side and the Ember side.
I have also included code for the Post model as it can have multiple attachments and multiple comments.
Rails code:
class Attachment < ActiveRecord::Base
belongs_to :user
belongs_to :attachable, polymorphic: true
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :attachments, as: :attachable
has_many :comments, as: :commentable
end
class ApplicationSerializer < ActiveModel::Serializer
embed :ids, include: true
end
class AttachmentSerializer < ApplicationSerializer
attributes :id, :url, :errors
has_many :comments
end
class CommentSerializer < ApplicationSerializer
attributes :id, :body, :created_at, :commentable_id, :commentable_type
has_one :user
end
class PostSerializer < ApplicationSerializer
attributes :id, :title, :body, :posted_at, :errors
has_one :user
has_many :attachments, embed: :objects, include: true
has_many :comments
end
class Api::V1::PostsController < Api::V1::BaseController
before_filter :auth_only!, only: :create
def create
# clean / capture ember-data supplied arguments
params[:post].delete(:user_id)
attachments_params = params[:post].delete(:attachments)
#post = current_user.posts.new(params[:post])
process_attachments(attachments_params)
if #post.save
render json: #post, status: 201
else
warden.custom_failure!
render json: #post, status: 422
end
end
protected
def process_attachments(attachments_params)
return unless attachments_params.present?
attachments_params.each do |attachment_params|
# ignore ember-data's additional keys
attachment_params.delete(:created_at)
attachment_params.delete(:user_id)
attachment = #post.attachments.new(attachment_params)
attachment.user = current_user
end
end
end
Ember code:
DS.RESTAdapter.configure 'App.Post',
alias: 'Post'
DS.RESTAdapter.map 'App.Post',
attachments: { embedded: 'always' }
App.Store = DS.Store.extend
adapter: DS.RESTAdapter.create
namespace: 'api/v1'
App.Comment = App.Model.extend
user: DS.belongsTo('App.User')
commentable: DS.belongsTo('App.Commentable', { polymorphic: true })
body: DS.attr('string')
createdAt: DS.attr('date')
App.Commentable = App.Model.extend
comments: DS.hasMany('App.Comment')
App.Attachment = App.Commentable.extend
user: DS.belongsTo('App.User')
url: DS.attr('string')
App.Post = App.Commentable.extend
user: DS.belongsTo('App.User')
attachments: DS.hasMany('App.Attachment')
title: DS.attr('string')
body: DS.attr('string')
postedAt: DS.attr('date')
App.PostFormOverlayController = App.OverlayController.extend
# 'files' attribute is set by a widget that wraps the filepicker.io JS
updateAttachments: (->
attachments = #get('attachments')
attachments.clear()
#get('files').forEach (file) =>
attachment = App.Attachment.createRecord({fpFile: file})
attachments.addObject(attachment)
).observes('files')
App.CommentNewController = App.ObjectController.extend
# this should be instantiated with it's model set to the commentable
# item. eg. `{{render 'comment/new' content}}`
save: ->
transaction = #get('store').transaction()
comment = transaction.createRecord App.Comment,
body: #get('body')
commentable: #get('model')
comment.one 'didCreate', #, ->
#set('body', null)
transaction.commit()
Unfortunately my Rails code has become a little polluted with ember-data specific oddities as it tries to send back all the attributes that are defined on the models. (Note: There is an open proposal for read-only attributes that would solve the params pollution issue)
If anyone knows a better way to approach any of the above please let me know!
NB: I'm a little concerned that having models extend from App.Commentable will prevent me from having multiple polymorphic attachments on a model, I may need to look for a different way of handling that.
The Comment part of Kevin Ansfield's answer (didn't use the Post part) works as of Ember 1.3.0-beta / Ember Data 1.0.0-beta.4 except for the rails serializer, which should now be:
class CommentSerializer < ActiveModel::Serializer
attributes :id, :body, :created_at, :commentable_id, :commentable_type
has_one :user
def attributes
data = super
data[:commentable] = {id: data[:commentable_id], type: data[:commentable_type]}
data.delete(:commentable_type)
data.delete(:commentable_id)
data
end
Related
For whatever reason code that use to work regarding following/follower no longer works, I suspect I need a cancancan rule, but I am not sure. Hopefully someone can see my mistake...
Profile model
class Profile < ActiveRecord::Base
...
has_many :follower_relationships, foreign_key: :following_id, class_name: 'Follow', dependent: :destroy
has_many :followers, through: :follower_relationships, source: :follower
has_many :following_relationships, foreign_key: :follower_id, class_name: 'Follow', dependent: :destroy
has_many :following, through: :following_relationships, source: :following
...
end
Follow model
class Follow < ActiveRecord::Base
...
belongs_to :follower, foreign_key: 'follower_id', class_name: 'Profile'
belongs_to :following, foreign_key: 'following_id', class_name: 'Profile'
...
end
Profiles Controller
class ProfilesController < ApplicationController
...
load_and_authorize_resource
...
def follow
if current_user.profile.follow(#profile.id)
SystemMailer.following_email(#profile.user, current_user).deliver_later
redirect_to request.referrer
end
end
def unfollow
redirect_to request.referrer if current_user.profile.unfollow(#profile.id)
end
current ability.rb
class Ability
include CanCan::Ability
...
can :edit, Profile, user_id: user.id
can :read, Profile
can :update, Profile, user_id: user.id
can :show, Profile
can :manage, Profile, id: user.profile.id
I have tried various can abilities, but I feel like I am just hacking around. If there is no errors with the lines above, can someone point out the can rule that I should put in place?
Thanks!
I figured it out, it actually wasn't being blocked because of the model organization, the actions were not being permitted in the ability class.
adding:
can :follow, Profile
can :unfollow, Profile
Solved the issues. Sorry to waste everyone's time.
I have two model as:
Customer:
has_many :products, :dependent => :destroy
accepts_nested_attributes_for :products, reject_if: proc { |attributes| attributes['product_id'].blank? }
Product:
belongs_to :customer
products controller:
def product_params
params.require(:product).permit(:name, :first_build)
end
customers controller:
def customer_params
params.require(:customer).permit(:first_build, :name, :product_id,
products_attributes: [:first_build, :customer_id])
end
So in the customers controller I do this
#customer.products.build(:first_build => true)
but I get this error
unknown attribute 'first_build' for Prodcut
but when I do this #customer.products.build(:name => "test product name")
it works perfectly without any error. One thing to note here, first_build is not a column in the products table.
If you want to pass attributes that are not in the table, you can create a "temporary" attribute which will not be stored in the table but will be available while the object is in memory.
class Product < ActiveRecord::Base
attr_accessor :first_build
...
end
I need help with a create form for a has_many_through association which also specifies a class name:
class Sponsor < ActiveRecord::Base
has_many :member_contacts
has_many :contacts, through: member_contacts, class_name: :member
accepts_nested_attributes_for :address, :contacts
end
class Contact < ActiveRecord::Base
has_many :member_contacts
has_many :sponsors, through: member_contacts
end
class MemberContact < ActiveRecord::Base
belongs_to :contact
belongs_to :sponsor
end
sponsors_controller.rb
def create
#sponsor = Sponsor.create(sponsor_params)
#sponsor.contacts.build
end
def sponsor_params
params.require(:sponsor).permit(:name,:website,:email,
:contact_first_name, :contact_surname, contacts_attributes: [], address_attributes: [:flat, :street, :postal_code, :city])
end
sponsor/_form.html.haml
= simple_form_for #sponsor do |f|
= f.association :contacts, collection: Member.all, label_method: :full_name
This is failing with the error 'unpermitted params, contact_ids', because
"contact_ids"=>["", "4", "5", "6"]
is being passed in the params hash. In the form I'd like a drop down list of all the members and the ability to select multiple members which will be saved against the sponsor as contacts.
How do I set up the contacts_attributes in sponsor_params in the controller and the collection_select simple_form helper in the view?
To get the form working, I added a foreign key to the sponsor class
has_many :contacts, through: member_contacts, class_name: 'Member', foreign_key 'member_id'
changed the strong params in the controller
def sponsor_params
params.require(:sponsor).permit(:name,:website,:email, contact_first_name, :contact_surname, contact_ids: [], address_attributes: [:flat, :street, :postal_code, :city])
end
and removed the association in the view, using collection_select
= f.collection_select :contact_ids, Member.all, :id, :full_name,
{ selected: #sponsor.contacts.map(&:id) }, { multiple: true }
You can set like this:
params.require(:sponsor).permit(:name,:website,:email, contact_ids: []...)
Note that permit(:contact_ids) will fail, but permit(contact_ids: []) works.
Using ember-data, I have this two models:
App.Post = DS.Model.extend
title: DS.attr "string"
body: DS.attr "string"
categories: DS.hasMany "App.Category"
App.Category = DS.Model.extend
name: DS.attr "string"
posts: DS.hasMany 'App.Post'
and this serialization:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :categories
embed :ids, include: true
end
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name
end
When I ask for posts, I get the expected JSON and I can access a post's categories without problem, but if I request the categories (that I think that they are cached) I get the categories without any relation to posts. It doesn't even try to make a get request (that wouldn't work either).
So, shouldn't categories have their posts relations filled up?
Not sure if I miss something in ember or AMS (which I think that the category serializer should know that has many posts)
Well, after struggling with some guys at IRC I ended with this solution, which I hope it will be helpful for others and maybe improved.
The problem was that the categories doesn't had any post reference, So if you ask for Posts, you get the posts with categories, but the categories themselves knows nothing about posts.
If I try to do something like:
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name
has_many :posts
embed :ids, include: true
end
it will explode because they are referencing each other and you get a "Too deep level" or something like that.
You can do something like:
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name
has_many :posts, embed: :objects
end
and it will work, but the result JSON will be huge because when you request posts, you get every post + every comment and inside them, every post that have that category... No love
So what's the idea? Having something like:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :categories
embed :ids, include: true
end
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name
has_many :posts, embed: :ids
end
For every post you get the categories_ids and for every category you reference, you only get its attributes and the ids (not the entire objects) of the posts that belongs to that category.
But what happens when you go to '/#/categories' and you didn't loaded the posts yet? Well, since your CategorySerializer doesn't serialize any post, you won't get anything.
So since you can't do cross references between serializers, I ended with 4 serializers. 2 for posts and their categories and 2 for categories and their posts (so, doesn't matter if you load the posts first or the categories):
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :categories, serializer: CategoriesForPostSerializer
embed :ids, include: true
end
class CategoriesForPostSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :posts, embed: :ids
end
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name
has_many :posts, serializer: PostsForCategorySerializer
embed :ids, include: true
end
class PostsForCategorySerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :categories, embed: :ids
end
This does the trick. But since I'm new with Ember and I'm not a crack of JSON design. If someones knows a simple way or maybe doing some embedded (always or load in the adapter, which I don't understand yet), please comment :)
I am having an issue with trying to save many to many relationships in Ember.js using ember-data and rails. The association works fine on the ember side of things, however when I try to commit the transaction it will not include the list of new associations when submitting to rails. Any help would be much appreciated, I've been tearing out my hair trying to find an example application that does something this simple on github but I can't seem to find one.
Here is a dumbed down version of my Ember code:
App.Store = DS.Store.extend
revision: 11
adapter: DS.RESTAdapter.create
url: '/api'
App.Router.map ->
#resource 'rosters'
#resource 'users'
App.User = DS.Model.extend
rosters: DS.hasMany 'App.Roster'
App.Roster = DS.Model.extend
users: DS.hasMany 'App.User'
App.RostersEditRoute = Ember.Route.extend
setupController: (controller, model) ->
controller.set 'users_list', App.User.find()
App.RostersEditView = Ember.View.extend
userCheckbox: Em.Checkbox.extend
checkedObserver: ( ->
users = #get 'roster.users'
if #get 'checked'
users.addObject #get 'user'
else
users.removeObject #get 'user'
#get('controller.store').commit()
).observes 'checked'
Edit form:
each user in users_list
label.checkbox
view view.userCheckbox rosterBinding="current_roster" userBinding="user" | #{user.full_name}
Rails Backend (Using Inherited Resources and Active Model Serializer gems):
class User < ActiveRecord::Base
has_many :rosters_users
has_many :rosters, through: :rosters_users
accepts_nested_attributes_for :rosters
end
class RostersUser < ActiveRecord::Base
belongs_to :user
belongs_to :roster
end
class Roster < ActiveRecord::Base
has_many :rosters_users
has_many :users, through: :rosters_users
accepts_nested_attributes_for :users
end
module Api
class RostersController < BaseController
end
end
module Api
class UsersController < BaseController
end
end
module Api
class BaseController < ActionController::Base
inherit_resources
respond_to :json
before_filter :default_json
protected
# Force JSON
def default_json
request.format = :json if params[:format].nil?
end
end
end
class UserSerializer < BaseSerializer
has_many :rosters, embed: :ids
end
class RosterSerializer < BaseSerializer
has_many :users, embed: :ids
end
So like I said, the association works fine on Ember but rails doesn't seem to be getting the new association data. When I check out the XHR tab in the web inspector, I see it only sent this:
Request Payload
{"roster":{"created_at":"Thu, 21 Mar 2013 23:16:02 GMT","team_id":"1"}}
And I am returned with a 204 no content error because nothing changed.
Despite the fact that it's possible to set up matching hasMany relationships, Ember Data doesn't actually support many to many relationships yet (see this issue). What you can do for now is decompose the relationship using a membership model.
App.User = DS.Model.extend
rosterMemberships: DS.hasMany 'App.RosterMembership'
App.RosterMembership = DS.Model.extend
user: DS.belongsTo 'App.User'
roster: DS.belongsTo 'App.Roster'
App.Roster = DS.Model.extend
rosterMemberships: DS.hasMany 'App.RosterMembership'
Now you can use createRecord() and deleteRecord() with the membership model to add and delete relationships.
Unfortunately, in this example, it's not so easy to bind to the collection of rosters for a particular user. One work-around is as follows:
App.User = DS.Model.extend
rosterMemberships: DS.hasMany 'App.RosterMembership'
rosters: ( ->
#get('rosterMemberships').getEach('user')
).property 'rosterMemberships.#each.relationshipsLoaded'
App.RosterMembership = DS.Model.extend
user: DS.belongsTo 'App.User'
roster: DS.belongsTo 'App.Roster'
relationshipsLoaded: ( ->
#get('user.isLoaded') and #get('roster.isLoaded')
).property 'user.isLoaded', 'roster.isLoaded'
If you bind to user.rosters, then your template should update when relationships are created or destroyed.
Alternate solution:
App.Roster = DS.Model.extend
users_map: DS.attr('array')
users: DS.hasMany 'App.User'
users_change: (->
#set('users_map', #get('users').map((el) -> el.id).toArray())
).observes('users.#each')
to server in POST data will sending array of users ids