Don't build another association if one already exists - ruby-on-rails

I have some problems getting a nested form working. In the below example I have a User that can define multiple custom labels for a post. The user should be able to enter a value for each particular label.
So one Post can have multiple labels, but should only have one value for each label! In example: Post can have a label named "Date" and also have a label named "Mood". For both labels there should be just one value.
The problem is when a User creates a Label -let say "Date"- it should only be possible to enter one value for this post for this particular label. So if a value for date is given, the form shouldn't build another field for date again. (the post already has a date)
User creates custom labels (this works)
On the edit page of the Post, User sees the labels he created in step 1 (this works)
User can enter a value for each of the Label (here is the problem)
I have the following models:
class User < ActiveRecord::Base
has_many :posts
has_many :labels
end
class Post < ActiveRecord::Base
has_many :label_values
belongs_to :user
accepts_nested_attributes_for :label_values, allow_destroy: true
end
class Label < ActiveRecord::Base
has_many :label_values
belongs_to :user
end
class LabelValue < ActiveRecord::Base
belongs_to :post
belongs_to :label
end
In my controller I have
class PostsController < ApplicationController
def edit
#labels = current_user.labels.all
#post.label_values.build
end
end
My form:
= simple_form_for #post do |f|
=f.fields_for :label_values do |s|
=s.association :label, :include_blank => false
=s.input :value
= f.button :submit
Like this, every time a User enters a value for a particular label, the next time a new label_value is build again and that is not what I want. For each label the User should be able to enter one value.

If you intend to use Label to attach metadata to Post you may want to consider using a relationship somewhat like the following:
class User < ActiveRecord::Base
has_many :user_labels # Labels which have been used by this user.
has_many :labels, through: :user_labels
has_many :posts
end
class Post < ActiveRecord::Base
has_many :post_labels
belongs_to :user, as: :author
has_many :labels, through: :post_labels
accepts_nested_attributes_for :post_labels
def available_labels
Label.where.not(id: labels.pluck(:label_id))
end
end
class Label < ActiveRecord::Base
# #attribute name [String] The name of the label
has_many :user_labels
has_many :post_labels
has_many :users, through: :user_labels
has_many :posts, through: :post_labels
validates_uniqueness_of :name
end
# Used to store label values that are attached to a post
class PostLabel < ActiveRecord::Base
belongs_to :post
belongs_to :label
validates_uniqueness_of :label, scope: :post # only one label per post
# #attribute value [String] the value of the label attached to a post
end
# Used for something like displaying the most used labels by a user
class UserLabel < ActiveRecord::Base
belongs_to :user
belongs_to :label
end
In this setup labels act somewhat like "tags" on Stackoverflow. Each separate label is unique in the system but may be attached to many posts - by many users.
Note the validates_uniqueness_of :label, scope: :post validation which ensures this.
The actuals values attached to a tag are stored on the value attribute of PostLabel. One major drawback to this approach is that you are really limited on by type of the PostLabel value column and may have to do typecasting. I really would not use it for dates as in your example as it will be difficult to do a query based on date.
def PostController
def new
#available_labels = Label.all()
end
def edit
#post = Post.joins(:labels).find(params[:id])
#available_labels = #post.available_labels
end
def create
#post = Post.new(create_params)
if (#post.save)
#post.labels.each do |l|
#post.user.labels << l
end
# ...
else
# ...
end
end
private
def create_params
params.permit(:post).allow(:post_labels)
end
end
Added
This is simply some opinionated recommendations:
To add new labels to a post I would add a text input below the already assigned labels.
I would autocomplete using GET /labels. and filter against the inputs already present in the form. You can allow users to create new labels on the fly by doing an ajax request to POST /labels.

I finally came to something. In the Post model I created:
def empty_labels
Label.where.not(:id => labelValue.select(:label_id).uniq)
end
def used_simfields
LabelValue.where(post_id: id)
end
Then in the view I did:
= simple_form_for #post do |f|
-#post.used_labels.each do |used_label|
=f.fields_for :label_values, used_label do |old_label|
=old_label :value
-#post.empty_labels.each do |empty_label|
=empty_label.fields_for :label_values, #post.label_values.new do |new_label|
=new_label.association :label
=new_label.input :value
I am very sure there are nicer ways to achieve this, so new ideas are very welcome.

Related

Setting a parameter on a join table in rails-admin

I have a model:
class Delivery < ActiveRecord::Base
has_and_belongs_to_many :dropoff_points, :join_table => :delivery_dropoff_points
end
This model has a field scheduled_date.
This is a DeliveryDropoffPoint class:
class DeliveryDropoffPoint < ActiveRecord::Base
belongs_to :delivery
belongs_to :dropoff_point
end
I need to make sure that when I create a record in rails-admin on any given day the dropoff point gets one and only one delivery.
I tried to implement before_save filter:
class DeliveryDropoffPoint < ActiveRecord::Base
belongs_to :delivery
belongs_to :dropoff_point
def set_scheduled_date
self.scheduled_date = delivery.scheduled_date
end
before_save :set_scheduled_date
validates_uniqueness_of :branch_floor, :scope => :scheduled_date, :message => "floor can only have one job assigned on a given day"
end
However, rails-admin seemed to ignore it, regretfully. Is there any other way to set the value of a column in a join table when selecting the items in the select-box in rails-admin?
Thanks!

Ruby: How do I assign values in the controller when using nested forms?

I have 3 models: Employers, Partners and Collaborations.
As an Employer, I want to add a record to my Partner model and to my Collaboration model to be able to indicate a collaboration between a Partner and a Employer. I therefore have the following columns in my database/tabels.
Models
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :partners, :through => :collaborations
end
class Partner < ActiveRecord::Base
has_many :collaborations
has_many :employers, :through => :collaborations
accepts_nested_attributes_for :collaborations
end
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
end
Tables
Collaborations
employer_id:integer
partner_id:integer
tarive:string
Partners
added_by:integer
name:string
Because I want to be able to add a Partner/Collaboration within 1 form, I use nested forms. So I can add a partner (name, etc) and a collaboration (tarive, etc) in one go.
My (simple_form) form looks like this (I have named_space resource).
Te reduce clutter, I removed as much HTML mark_up as I could, this is not the issue.
Form
/views/employer/partners/_form
= simple_form_for [:employer, #partner], html: { multipart: true } do |f|
Partner
= f.input :name, input_html: { class: 'form-control' }
= f.simple_fields_for :collaborations do |ff|
Tarive
= ff.input :tarive, input_html: { class: 'form-control' }
= f.button :submit, "Save"
My controller looks like
class Employer::PartnersController < ActionController::Base
def new
#partner = Partner.new
#partner.collaborations.build
end
def create
#partner = Partner.new(partner_params)
#partner.collaborations.build
#partner.added_by = current_employer.id
#partner.collaborations.employer_id = current_employer.employer_id
#partner.collaborations.partner_id = #partner.id
#partner.collaborations.added_by = current_employer.id
if #partner.save
redirect_to employer_partner_path(#partner), notice: "Succes!"
else
render 'new'
end
end
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:id, :employer_id, :partner_id, :tarive])
end
end
Problem
The problem/question I have is this. The attributes are assigned nicely and added in the model. But I want to add a employer_id as well, which I have in current_employer.employer.id (Devise). I do not want to work with hidden forms, just to avoid this issue.
I assigned 'parent' models always like #partner.added_by = current_employer.id and that works beautifully.
When I use:
#partner.collaborations.employer_id = current_employer.employer_id
I get an error, saying #partner.collaborations.employer_id is empty.
Question
How can I assign a variable to the nested_form (Collaboration) in my controller#create?
Or more specifically: how can I assign current_employer.employer_id to #partner.collaborations.employer_id?
There are several ways:
Merge the params
Deal with objects, not foreign keys
Personally, I feel your create method looks really inefficient. Indeed, you should know about fat model skinny controller - most of your associative logic should be kept in the model.
It could be improved using the following:
#app/controllers/employers/partners_controller.rb
class Employers::PartnersController < ApplicationController
def new
#partner = current_employer.partners.new #-> this *should* build the associated collaborations object
end
def create
#partner = current_employer.partners.new partner_params
#partner.save ? redirect_to(employer_partner_path(#partner), notice: "Succes!") : render('new')
end
private
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:tarive]) #when dealing with objects, foreign keys are set automatically
end
end
This would allow you to use:
#app/views/employers/partners/new.html.erb
= simple_form_for #partner do |f| #-> #partner is built off the current_employer object
= f.input :name
= f.simple_fields_for :collaborations do |ff|
= ff.input :tarive
= f.submit
... and the models:
#app/models/partner.rb
class Partner < ActiveRecord::Base
belongs_to :employer, foreign_key: :added_by
has_many :collaborations
has_many :employers, through: :collaborations
accepts_nested_attributes_for :collaborations
end
#app/models/collaboration.rb
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
belongs_to :creator, foreign_key: :added_by
before_create :set_creator
private
def set_creator
self.creator = self.employer_id #-> will probably need to change
end
end
#app/models/employer.rb
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :employers, through: :collaborations
end
This may not give you the ability to set tarive, however if you cut down the manual declarations in your model, we should be able to look at getting that sorted.
The main thing you need to do is slim down your code in the controller. You're being very specific, and as a consequence, you're encountering problems like that which you mentioned.

Rails: includes with polymorphic association

I read this interesting article about Using Polymorphism to Make a Better Activity Feed in Rails.
We end up with something like
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
end
Now, if two of those subjects are for example:
class Event < ActiveRecord::Base
has_many :guests
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
class Image < ActiveRecord::Base
has_many :tags
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
With create_activities defined as
def create_activities
Activity.create(subject: self)
end
And with guests and tags defined as:
class Guest < ActiveRecord::Base
belongs_to :event
end
class Tag < ActiveRecord::Base
belongs_to :image
end
If we query the last 20 activities logged, we can do:
Activity.order(created_at: :desc).limit(20)
We have a first N+1 query issue that we can solve with:
Activity.includes(:subject).order(created_at: :desc).limit(20)
But then, when we call guests or tags, we have another N+1 query problem.
What's the proper way to solve that in order to be able to use pagination ?
Edit 2: I'm now using rails 4.2 and eager loading polymorphism is now a feature :)
Edit: This seemed to work in the console, but for some reason, my suggestion of use with the partials below still generates N+1 Query Stack warnings with the bullet gem. I need to investigate...
Ok, I found the solution ([edit] or did I ?), but it assumes that you know all subjects types.
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id
belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id
end
And now you can do
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
But for eager loading to work, you must use for example
activity.event.guests.first
and not
activity.part.guests.first
So you can probably define a method to use instead of subject
def eager_loaded_subject
public_send(subject.class.to_s.underscore)
end
So now you can have a view with
render partial: :subject, collection: activity
A partial with
# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject
And two (dummy) partials
# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>
# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works for me. Feel free to use my fork, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
You can use ActiveRecord::Associations::Preloader to preload guests and tags linked, respectively, to each of the event and image objects that are associated as a subject with the collection of activities.
class ActivitiesController < ApplicationController
def index
activities = current_user.activities.page(:page)
#activities = Activities::PreloadForIndex.new(activities).run
end
end
class Activities::PreloadForIndex
def initialize(activities)
#activities = activities
end
def run
preload_for event(activities), subject: :guests
preload_for image(activities), subject: :tags
activities
end
private
def preload_for(activities, associations)
ActiveRecord::Associations::Preloader.new.preload(activities, associations)
end
def event(activities)
activities.select &:event?
end
def image(activities)
activities.select &:image?
end
end
image_activities = Activity.where(:subject_type => 'Image').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => 'Event').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)
I would suggest adding the polymorphic association to your Event and Guest models.
polymorphic doc
class Event < ActiveRecord::Base
has_many :guests
has_many :subjects
after_create :create_activities
end
class Image < ActiveRecord::Base
has_many :tags
has_many :subjects
after_create :create_activities
end
and then try doing
Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
Does this generate a valid SQL query or does it fail because events can't be JOINed with tags and images can't be JOINed with guests?
class Activity < ActiveRecord::Base
self.per_page = 10
def self.feed
includes(subject: [:guests, :tags]).order(created_at: :desc)
end
end
# in the controller
Activity.feed.paginate(page: params[:page])
This would use will_paginate.

How to create a polymorphic model

I need to link Comments to a Post. However the Comment could be (user generated) a simple text, (system generated) a link or an (system generated) image.
At first they all shared the same attributes. So I just needed to create a category attribute, and do different stuff with the text attribute based on that category.
example:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author, :class_name => "User"
CATEGORY_POST = "post"
CATEGORY_IMAGE = "image"
CATEGORY_LINK = "link"
validates :text, :author, :category, :post, :presence => true
validates_inclusion_of :category, :in => [CATEGORY_POST, CATEGORY_IMAGE, CATEGORY_LINK]
attr_accessible :author, :text, :category, :post
def is_post?
self.category == CATEGORY_POST
end
def is_link?
self.category == CATEGORY_LINK
end
def is_image?
self.category == CATEGORY_IMAGE
end
end
However this wil not suffice now, because I doesn't feel clean to dump every value in a generic "text" property. So I was thinking about create a polymorphic model (and if needed in a factory pattern). But when I googled about polymorphic models, I get examples like a Comment on a Post, but the same Comment on a Page, kind of relations. Is my understanding of polymorphic different (a model that acts different in different situations, compared to a model that acts the same under different scopes)?
So how would I set up this kind of relationship?
I was thinking of (and please correct me)
Post
id
Comment
id
post_id
category (a enum/string or integer)
type_id (references either PostComment, LinkComment or ImageComment based on category)
author_id
PostComment
id
text
LinkComment
id
link
ImageComment
id
path
User (aka Author)
id
name
But I have no clue how to setup the model so that I can call post.comments (or author.comments) to get all comments. A nice to have would be that the creation of a comment would be through comment and not link/image/postcomment (comment acting as the factory)
My main question is, how to setup up the activerecord models, so the relations stay intact (a author has comments and a post has comments. Comments being either a Link, Image or Postcomment)
I'm going to answer only your main question, the model setup. Given the columns and tables you used in your question, with the exception of Comment, you can use the following setup.
# comment.rb
# change category to category_type
# change type_id to category_id
class Comment < ActiveRecord::Base
belongs_to :category, polymorphic: true
belongs_to :post
belongs_to :author, class_name: 'User'
end
class PostComment < ActiveRecord::Base
has_one :comment, as: :category
end
class LinkComment < ActiveRecord::Base
has_one :comment, as: :category
end
class ImageComment < ActiveRecord::Base
has_one :comment, as: :category
end
with that setup, you can do the following.
>> post = Post.first
>> comments = post.comments
>> comments.each do |comment|
case comment.category_type
when 'ImageComment'
puts comment.category.path
when 'LinkComment'
puts comment.category.link
when 'PostComment'
puts comment.category.text
end
end

Retrieving model attribute from table+column name

Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.

Resources