rails model association using has_many: through - ruby-on-rails

i have a lessons table and a tags table. i associate both of the them using a has_many :through relationship and my middle table is tags_relationship.rb
class Lesson < ActiveRecord::Base
attr_accessible :title, :desc, :content, :tag_name
belongs_to :user
has_many :tag_relationships
has_many :tags, :through => :tag_relationships
end
class Tag < ActiveRecord::Base
attr_accessible :name
has_many :tag_relationships
has_many :lessons, :through => :tag_relationships
end
in one of my views, im trying to create a a virtual attribute. i have...
<div class="tags">
<%= f.label :tag_name, "Tags" %>
<%= f.text_field :tag_name, data: { autocomplete_source: tags_path} %>
</div>
but my lessons table doesn't have that attribute, tag_name, so it calls my method instead
def tag_name
????????
end
def tag_name=(name)
self.tag = Tag.find_or_initialize_by_name(name) if name.present?
end
however im not sure what to put inside the ????????. im trying to refer the :name attribute inside my tags table.
back then i used a has_many and belongs_to relationship. my lesson belonged to a tag (which was wrong) but i was able to write...
tag.name
and it worked. but since its a has_many :through now, im not sure. i tried using tags.name, Lessons.tags.name, etc but i cant seem to get it to work. how can i refer to the tags table name attribute? thank you

Apologize for my bad english.
When your Lesson was belonged to Tag lesson had only one tag, so your code was right. But now Lesson has many Tags, and it is collection (array in simple words). So, your setter must be more complex:
def tag_names=(names)
names = if names.kind_of? String
names.split(',').map{|name| name.strip!; name.length > 0 ? name : nil}.compact
else
names
end
current_names = self.tags.map(&:name) # names of current tags
not_added = names - current_names # names of new tags
for_remove = current_names - names # names of tags that well be removed
# remove tags
self.tags.delete(self.tags.where(:name => for_remove))
# adding new
not_added.each do |name|
self.tags << Tag.where(:name => name).first || Tag.new(:name => name)
end
end
And getter method should be like this:
def tag_names
self.tags.map(&:name)
end
BTW, finders like find_by_name are deprecated. You must use where.

Related

Rails has_many relationship with prefilled views

I have a pretty basic Rails 4 app, and am using Cocoon's nested forms to manage the has_many... :through model association.
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
# ... etc
end
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
# ... etc
end
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluation, reject_if: :all_blank
# ... etc
end
When I use Cocoon in the View, I want to use the New Assessment view to pre-fill all the Student records in order to create a new Evaluation for each one. I don't want to have to do some hacky logic on the controller side to add some new records manually, so how would I structure the incoming request? With Cocoon I see that requests have some number in the space where the id would go (I've replaced these with ?? below).
{"utf8"=>"✓", "authenticity_token"=>"whatever", "assessment"=>{"description"=>"quiz 3", "date(3i)"=>"24", "date(2i)"=>"10", "date(1i)"=>"2015", "assessments_attributes"=>{"??"=>{"student_id"=>"2", "grade" => "A"}, "??"=>{"student_id"=>"1", "grade" => "B"}, "??"=>{"student_id"=>"3", "grade"=>"C"}}, }}, "commit"=>"Create Assessment"}
I see in the Coccoon source code that this is somehow generated but I can't figure out how it works with the Rails engine to make this into a new record without an ID.
What algorithm should I use (or rules should I follow) to fill in the id above to make a new record?
"??"
Never a good sign in your params.
With Cocoon I see that requests have some number in the space where the id would go
That ID is nothing more than the next ID in the fields_for array that Rails creates. It's not your record's id (more explained below).
From your setup, here's what I'd do:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
end
#app/models/evaluation.rb
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
end
#app/models/assessment.rb
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluations, reject_if: :all_blank
end
This will allow you to do the following:
#app/controllers/assessments_controller.rb
class AssessmentsController < ApplicationController
def new
#assessment = Assessment.new
#students = Student.all
#students.each do
#assessment.evaluations.build
end
end
end
Allowing you:
#app/views/assessments/new.html.erb
<%= form_for #assessment do |f| %>
<%= f.fields_for :evaluations, #students do |e| %>
<%= e.hidden_field :student_id %>
<%= e.text_field :grade %>
<% end %>
<%= f.submit %>
<% end %>
As far as I can tell, this will provide the functionality you need.
Remember that each evaluation can connect with existing students, meaning that if you pull #students = Student.all, it will populate the fields_for accordingly.
If you wanted to add new students through your form, it's a slightly different ballgame.
Cocoon
You should also be clear about the role of Cocoon.
You seem like an experienced dev so I'll cut to the chase - Cocoon is front-end, what you're asking is back-end.
Specifically, Cocoon is meant to give you the ability to add a number of fields_for associated fields to a form. This was discussed in this Railscast...
Technically, Cocoon is just a way to create new fields_for records for a form. It's only required if you want to dynamically "add" fields (the RailsCast will tell you more).
Thus, if you wanted to just have a "static" array of associative data fields (which is I think what you're asking), you'll be able to use fields_for as submitted in both Max and my answers.
Thanks to #rich-peck I was able to figure out exactly what I wanted to do. I'm leaving his answer as accepted because it was basically how I got to my own. :)
assessments/new.html.haml (just raw, no fancy formatting)
= form_for #assessment do |f|
= f.fields_for :evaluations do |ff|
.meaningless-div
= ff.object.student.name
= ff.hidden_field :student_id, value: ff.object.student_id
= ff.label :comment
= ff.text_field :comment
%br/
assessments_controller.rb
def new
#assessment = Assessment.new
#students = Student.all
#students.each do |student|
#assessment.evaluations.build(student: student)
end
end

How can I synchronize a has_many association using accepts_nested_attributes_for by foreign_key instead of ID?

I would like to synchronize a has_many association by foreign key. It seems I have to write custom code to do this. Is there any Rails / Active Record magic / Gem to achieve this? Specifically, I'd like to synchronize a join-table where the pairs of foreign keys should be unique.
class Food < ActiveRecord::Base
has_many :food_tags, :dependent=>:destroy, :inverse_of => :food
accepts_nested_attributes_for :food_tags, :allow_destroy => true
end
class FoodTag < ActiveRecord::Base
belongs_to :tag, :inverse_of=>:food_tags
belongs_to :food, :inverse_of=>:food_tags
end
class Tag < ActiveRecord::Base
has_many :food_tags, :dependent=>:destroy, :inverse_of=>:tag
has_many :foods, :through=>:food_tags
end
For my form with nested attributes (or my JSON API), I'd really like to omit the FoodTag id and use the tag_id to synchronize when updating a food.
I want to submit this on update to show that the tag is set or cleared
# this one is set
food[food_tag_attributes][0][tag_id] = 2114
food[food_tag_attributes][0][_destroy] = false
# this one is cleared
food[food_tag_attributes][1][tag_id] = 2116
food[food_tag_attributes][1][_destroy] = true
Instead, I have to submit this for update:
# this one is set
food[food_tag_attributes][0][id] = 109293
food[food_tag_attributes][0][tag_id] = 2114
food[food_tag_attributes][0][_destroy] = false
# this one is cleared
food[food_tag_attributes][0][id] = 109294
food[food_tag_attributes][1][tag_id] = 2116
food[food_tag_attributes][1][_destroy] = true
This pushes a burden to the client to know the IDs of the food tag records instead of just being able to Set or Clear tags based on the tag id.
Can this be done easily? I'm sure I could write a before_save filter on Food, but it seems like there should be a reasonably generic solution.
There is an option called index: for fields_for in the view helper. You can set the index as your foreign_key. Then instead of sequential or some arbitrary numbers, your foreign_key will be used as the key to refer to your object.
EDIT:
<%= form_for #person do |person_form| %>
<%= person_form.text_field :name %>
<% #person.addresses.each do |address| %>
<%= person_form.fields_for address, **index**: address.id do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
<% end %>

How can I create dynamic form field to store/update hash sets in Rails?

In my reservations table I have a rooms (text) field to store hash values such (1 => 3) where 1 is roomtype and 3 corresponds to the amount of rooms booked by the same agent.
My Reservation model
serialize reserved_rooms, Hash
Here is my nested resource
resources :hotels do
resources :roomtypes, :reservations
end
RoomType stores a single room type which belongs to Hotel model. Though I can enlist roomtypes within my reservation form I do not know how I can create a dynamic hash via form to create/update this hash.
I have this but I am looking for a way to create a dynamic hash "key, value" set. Meaning, if Hotel model has two RoomType my hash would be {12 = > 5, 15 => 1} (keys corresponds to the roomtype_ids while values are the amount}
<%= f.fields_for ([:roomtypes, #hotel]) do |ff| %>
<% #hotel.roomtypes.each do |roomtype| %>
<%= ff.label roomtype.name %>
<%= f.select :reserved_rooms, ((0..50).map {|i| [i,i] }), :include_blank => "" %>
<% end %>
<% end %>
What I want is what this website has in the availability section (nr. of rooms):
specs: rails 4.1, ruby 2.1
Note: If you think there is a design problem with this approach (storing reserved_room in a serialized field) I can follow another path by creating another table to store the data.
Might need tweaking but i used similar code with check-boxes and it worked!
<% #hotel.roomtypes.each do |roomtype| %>
<%= f.label roomtype.name %>
<%= f.select :"reserved_rooms[roomtype.id]", ((0..50).map {|i| [i,i] }), :include_blank => "" %>
<% end %>
This gets messy enough that I would probably consider going with a separate models as you mentioned. I would simply do:
class Hotel < ActiveRecord::Base
has_many :room_types
has_many :rooms, :through => :room_types
end
class RoomType < ActiveRecord::Base
has_many :rooms
end
class Room < ActiveRecord::Base
has_many :reservations
belongs_to :room_type
end
class Reservation < ActiveRecord::Base
belongs_to :room
belongs_to :agent
end
class Agent < ActiveRecord::Base
has_many :reservations
end
Then just use a generic form to submit the # Rooms integer, and let your controller handle making multiple reservations...? Maybe I'm not understanding your objective well enough...
Rails 4 has a new feature called Store you would love. You can easily use it to store a hash set which is not predefined. You can define an accessor for it and it is recommended you declare the database column used for the serialized store as a text, so there's plenty of room. The original example:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ], coder: JSON
end
u = User.new(color: 'black', homepage: '37signals.com')
u.color # Accessor stored attribute
u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
# There is no difference between strings and symbols for accessing custom attributes
u.settings[:country] # => 'Denmark'
u.settings['country'] # => 'Denmark'

Rails - List separated by commas through a join table

I have three tables, one of which is a join table between the other two tables.
Jobs: id
Counties: id
Countyizations: job_ib, county_id
I want to create a list of counties a specific job has associations with. I'm trying to use something like:
<%= #counties.map { |county| county.id }.join(", ") %>
But this obviously is not using the countyizations table. How can I change the above code to accomplish what I need? Also, I'd like to list the counties alphabetically in ASC order.
P.S.
I suppose I should have added how I'm linking my tables in my models.
Jobs: has_many :countyizations & has_many :counties, :through => :countyizations
Counties: has_many :countyizations & has_many :jobs, :through => :countyizations
Countyizations: belongs_to :county & belongs_to :job
For a given job.id you can use this this return all the counties filtered by the given job.
<%= #counties.order('name asc').includes(:jobs).where('jobs.id = ?', job.id) %>
Replace job.id based on your requirement, you could set a #job instance variable in the controller and use in the view instead.
Or even better move this code to controller action:
# controller
def show
job_name = ...
#counties = ...
#county_jobs = #counties.order('name asc').includes(:jobs).where(jobs.name = ?', job_name)
end
Then in your view, to show all the counties that have the searched job:
<%= #counties.map(&:id).join.(',') %>
I am not sure if I understand you correctly. Is the following what you want?
class Job < ActiveRecord::Base
has_many :countries, :through => :countyizations
end
class County < ActiveRecord::Base
has_many :jobs, :through => :countyizations
end
<%= #job.counties.sort{|a, b| a.name <=> b.name}.map{ |county| county.name }.join(", ") %>
I think use "has_many_and_belongs_to" instead "of has_many" may work also.

Scope exception rules

I am trying to define an inventory for all my articles but I want to exclude the articles that are sent to me with a parameter.
Here is what the relationship looks like:
Article
has_many :tags, through: :articletags
ArticleTags
belongs_to :article
belongs_to :tags
Tags
has_many :article, through: articletags
Here's a method to define the one without the tags in my models:
def self.by_not_tags(tag)
joins(:tags).where('tags.title != ?', tag)
end
Here's how I call it in my view:
<%= link_to (tag.title), articles_path(:scope => tag.title) %>
Here's my controller:
def custom
if params[:scope].nil?
#articles = Article.all(:order => 'created_at DESC')
else
#articles = Article.by_tags(params[:scope])
#articles2 = Article.by_not_tags(params[:scope])
end
end
The goal is to see all the articles with a tag first, and then to show the other ones without that tag, so I don't have duplicates.
My issue is with the joins, but I am not sure how to find the article without tags. Maybe an except would work, but I am not sure what kind of query would work for it.
Assuming ArticleTag model needs to validate the presence of both article_id and tag_id,
Article.where('article_tag_id is null')
If I do not assume that above validation stated,
Article.where('not exists (select 1 from article_tags where article_id = articles.id)')

Resources