has_many through and partials - ruby-on-rails

I have a User model, a Post model, and an Interest model.
User has_many posts through interests
User has_many interests
Post has_many users through interests
Post has_many interests
Interest belongs to Post
Interest belongs to User
Application_Controller is as follows:
class ApplicationController < ActionController::Base
before_filter :login_from_cookie
before_filter :find_user_interests
helper :all # include all helpers, all the time
session :session_key => '_blah_session'
include AuthenticatedSystem
def find_user_interests
#user_interests = current_user ? current_user.interests : []
true
end
end
Application.html.erb has as follows:
<%= render :partial => "users/interests", :object => #user_interests %>
_interests.html.erb partial is as follows:
ul
<% unless current_user.nil? then -%>
<% #user_interests.each do |interest| -%>
li<%= interest.post.title %>/li
<% end %>
<% end -%>
/ul
Given all this when I at localhost:3000/posts/1 my partial shows up fine, but when in localhost:3000/posts I get an error undefined method 'title' for nil:NilClass thus an error in the line li<%= interest.post.title %>/li shown above in the _interests.html.erb partial.
What the heck would be the issue?
TIA

That just means that one of the interests doesn't have an associated post on the other end. Most likely it was deleted. This could have been prevented by the following:
class Post < ActiveRecord::Base
has_many :interests, :dependent => :destroy
end
In the meantime you should clean up the orphans in the database.
Edit: You claim this was already in your model, but if it was then it's not clear how you could have an orphaned Interest as the error indicates. Maybe it was created before you added the dependent clause? Again, go delete the orphans via SQL and then try again. If the problem resurfaces later you must be deleting without callbacks somewhere.
Regarding your size problem. You could be using current_user.interests.count. This is due to some magic with Rails associations. count is a special method on a Rails association that runs SQL. length is just an array method telling you how many items are in the array. Rails associations have a few special methods, but the rest of them they pass through to the array object transparently.
Further critiques: when you pass :object => #user_interests you are setting a variable with the name of the partial. So you could reference the local variable interests in the partial. However you are referencing #user_interests, so passing the object is not necessary. All else being equal, passing the object and using a local variable is probably better (it's more explicit, more of a functional-programming style), but in this case you are not making use of that.
Finally, stylewise, I may be wrong since I don't have the full context, but in general I would put the logged_in condition in the template rather than setting user_interests to an empty array if there is no logged in user. This would allow you to reference current_user.interests.count in the template and independently set the interests to be displayed (eg. for pagination) in #user_interests.

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Finding if a user has already flagged an object

I've currently setup an instance where a Review has many Flags (via polymorphism) and a User has many flags. Everything works so far but I'd now like to make so that in the view if a user has flagged a particular review a message shows up, Flag Submitted instead of a link to flag it.
Right now I'm doing this by first gathering all of the flags that belong to the review then using any? to determine if any of them have the current user's id. I was wondering if there was a faster/more efficient way in doing this? It's the lookup chain that makes me comprehensive to leave it as is.
Note: flaggable is a local variable which in this case represents a #review:
<%= render "flag", flaggable: #review %>
class Review < ActiveRecord::Base
has_many :flags, as: :flaggable, dependent: :destroy
end
class Flag < ActiveRecord::Base
belongs_to :flaggable, polymorphic: true
belongs_to :owner, class_name: "User", foreign_key: :user_id
end
class User < ActiveRecord::Base
has_many :flags
end
<div>
<% if user_signed_in? && flaggable.flags.any? { |r| r.user_id == current_user.id } %>
<p>Flag Submitted</p>
<% else %>
<%= link_to "Flag", [:new, flaggable, :flag], remote: true %>
<% end %>
...
</div>
I think this will work:
<% if user_signed_in? && flaggable.flags.exists?(owner: current_user) %>
any? is an Array method that loops to check if any condition will return true for each element in the Array which is currently held in-memory.
Take note that flaggable.flags is an ActiveRecord::Relation object which extends the Array class, therefore you can use any Array methods for this. However, once you invoke any Array method, all flaggable.flags will be stored in-memory, which is by then, and only at that time will the loop to check matching conditions will take place.
exists? is an ActiveRecord method similar to where and find methods, in which this checks if any condition will return true for each row in the Table in the Database which is not in-memory, and is more optimised for searching, and therefore more efficient.
UPDATE: (To Explain Further)
Let's say you have this code
1 #users = User.where(is_admin: true)
2 #users = #users.where(age: 20)
3 <% #users.each do |user| %>
4 <%= user.name %>
5 <% end %>
6 <%= 'THERE IS A SANTA USER' if #users.any?{|user| user.name == 'Santa'} %>
At line 1, #users is an ActiveRecord::Relation, in which it does not yet access the DB. It only still currently is storing the DB Query in memory.
At line 2, #users has been added a condition that age should be 20, so the query now becomes something like #users = users-who-are-admin-and-age-is-20. This still is a Query stored in memory. Still no DB access happening yet.
At line 3, .each is called from #users. By calling this array method, #users which is an ActiveRecord::Relation now connects and queries the DB, which then returns an Array of users now available for looping as you would expect in a .each method. (You will notice that lines starting from 3 are code in the view file)
At line 6, .any? is called from #users. Since, #users which is still an ActiveRecord::Relation but has already accessed database and the Array is already in memory (because of line 3), then .any? will no longer access the DB, and will just behave like a normal .any? method of an Array class. You will then realize that if there are millions of User records in the DB, then #users will occupy a huge amount of memory which is not very good. It will be better to use .find_each which stores in-memory partial-by-partial with the downside of more DB calls.
On a side note, be careful of using .joins as opposed to .includes. .joins returns an Array object, while .includes returns an ActiveRecord::Relation. Both have their own use-cases.

Ruby on Rails 4 find object by id

I have the following show-view, where i display basic information about Product and display other User's Products.
<h1>Book <%= #product.name %></h1>
<% #products.each do |product| %>
<ul>
<%= product.name %>
<%= link_to "Make offer", {controller: "offers", :action => 'create', id: product.id } %>
</ul>
Controller
def show
#product = current_user.products.find(params[:id])
#products = Product.all
end
My goal is to make Offer between two Products.
I created Offer model and methods for making Offers:
class Offer < ActiveRecord::Base
belongs_to :product
belongs_to :exchanger, class_name: "Product", foreign_key: "exchanger_id"
validates :product_id, :exchanger_id, presence: true
def self.request(product, exchanger)
unless product == exchanger or Offer.exists?(product, exchanger)
transaction do
create(product: product, exchanger: exchanger, status: "oczekujace")
create(product: exchanger, exchanger: product, status: "oferta")
end
end
#other methods
end
Making offers is working, because I checked it in Console.
My problem is in OffersController:
class OffersController < ApplicationController
before_filter :setup_products
def create
Offer.request(#prod, #exchanger)
redirect_to root_path
end
private
def setup_products
#prod = current_user.products.find(1)
#exchanger = Product.find_by_id(params[:id])
end
end
Problem is with a following line (using link in show-page for products with different id's than 1 works):
#prod = current_user.products.find(1)
But I don't know how to find object in Db for actual product which my show-page shows. Not only for id = 1.
I don't know how to find this object in database.
I don't know the specific answer to your question, but perhaps if I explain what you need to look at, your solution will arise:
Find
Rails isn't magic - it uses ActiveRecord (which is an ORM - Object-Relation Mapper), which means every time you fire a query (through find), your ORM (ActiveRecord) will search the relevant database data for you
The problem you have is that although you're using the correct syntax for your lookup, you may not have a record with an id of 1 in your db.
current_user.products.find(1) tells ActiveRecord to scope the query around the current user, and their products. So you'll get something like like this:
SELECT * FROM 'products' WHERE user_id = '15' AND id = '1'
Objects
Further, you have to remember that Ruby (and Rails by virtue of being built on Ruby) is an object orientated language. This means that everything you load / interact with in the language should be based on an object
The problem you have is you're not associating your object to your Rails framework correctly. What I mean here is described below, but essentially, if you build your Rails framework correctly, it will give you the ability to associate your objects with each other, allowing you to call the various products you need from your offer
This is a simplistic way of looking at it, of course. You'll want to look at this diagram to see how it works:
Bottom line - try treating your application like a series of objects, rather than a logical flow. This will help you appreciate the various associations etc that you need to get it moving forward
Resources
You mention you can't show the product on your show page for an id other than one. I think the problem is really about how to get your show action to work.
If this is the case, let me explain...
Rails is resource-based, meaning that everything you do / create needs to be centred around a resource (object) of some sort. The problem is many people don't know this, and consequently complicate their controller structure for no reason:
Above is the typical "CRUD" routing structure for Rails-based resources. This should demonstrate the way that Rails will typically be constructed -- around resources
--
Further, Rails is built on the MVC programming pattern - meaning you need to use your controller to populate a series of data objects for use in your application.
To this end, if you load a resource, and want to populate it with resourceful information of another object - you need to make sure you have set up the data objects in a way to ensure you can look them up correctly, which either means passing the data through your routes or using a persistent data-type, such as cookies or sessions
The problem you have is you need to pass the product id to your controller somehow. How I'd do that is as follows (using nested resources):
#config/routes.rb
resources :offers do
resources :products #-> domain.com/offers/2/products
end
This will give you the ability to load the products controller with the variables params[:id] for the product, and params[:offer_id] for your Offer made available:
#app/controllers/products_controller.rb
Class ProductsController < ApplicationController
def show
#offer = Offer.find params[:offer_id]
#product = Product.find params[:id]
end
end

Creation of object which doesn't have model, while creation need to create several entries in other tables

In application user can enter new post which contain title, content of the post and category of post. So creating new post will be through some simple html form with few fields. Now i don't know where to put logic for creating new post for following reasons:
Post(or posts collection) is object which is constructed from different tables, for example.
#posts = User.joins(entries: [{storage: :vote}, :category])
.where("votes.count > ?", 0)
.select("users.username AS username,
storages.id AS storage_id,
storages.title AS title,
storages.content AS content,
votes.count AS votes,
categories.category_name AS category_name")
.order("votes.count DESC")
So when user create new post application must create new entries in different tables:
1.Create new entry in entries table. (id, user_id, category_id)
2. Create new entry in storages table.(id, title, content, entry_id)
3. Create new entry in vote table.(id, count, storage_id)
In situation where post is model i can use something like resources: posts then in posts controller through new and create i can create new post, but what in situation like this where i don't need posts controller nor post model? So, question is: which place is more appropriate to put logic for creating new post? Q1
My solution is to craete Storages controller with resource: storages, :only => [:new, :create] then through new and create of this controller to populate different tables in db? I'm forcing here only because i dont see any point of other CRUD actions here(like showing all or one storage), because i will not use storages individually but in conjunction with other tables. So from views/storages through new.html.erb and create.html.erb i can construct new post? Q2
Another solution is to create Post controller which doesn't have "corresponding" post model as i stated before. Here i'm guessing i can't use restful routes(CRUD) because there is not :id of post? I only can make manually non-restful routes like:
post 'posts/create/:title/:content/:category' => 'posts#create', :as => 'create_post' then from params[:title], params[:content] and params[:category] to populate other tables. Q3
Im new to rails so dont yell please :D
This sound like a call for nested forms, its covered in a screen cast
here.
You use the resources of the top model, in this case Entry.
and drill down to the 3rd model.
Simple sample of what to do is bellow.
Model should look like so,
entry.rb
class Entry < ActiveRecord::Base
has_many :storages, :dependent => :destroy
accepts_nested_attributes_for :storages, :allow_destroy => true
end
storage.rb
class Storage < ActiveRecord::Base
belongs_to :entry
has_many :votes, :dependent => :destroy
accepts_nested_attributes_for :votes, :allow_destroy => true
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :storage
end
and the form should look like so, in simple_form style
<%= simple_form_for #entry do |f| %>
<%= f.simple_fields_for :storages do |storage_fields| %>
<%= storage_fields_for :votes do |vote_fields| %>
<% end %>
<% end %>
<% end %>
and if you have all the models set up, you shouldn't have to do anything to the controller.
This approach is also nice because you can add multiple storages and votes ajax style(without reloading the page) if needed, which is always nice.
I'd use a form class instead of nested attributes any day, see http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ for an example of this pattern (Chapter "3. Extract Form Objects")
I've used the pattern often enough to gemify it https://github.com/bbozo/simple_form_class and it's used roughly in this way: https://gist.github.com/bbozo/5036937, if you're interested to use it I'll push some docs
EDIT: reason why I tend to go the form class route most of the time is because nested attributes never failed to bite me in the end, either because strong parameter handling got cumbersome, or validators get too complicated (if persisted? else ...), or persistence logic needs to be extended to support some little knack that resolves into callback hell or recursive saves in the model (before/after save spaghetti troubles)

Best practice: How to split up associations-functions in controllers with equal-access models

I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.

Resources