has_many through but with unique source and multiple associations - ruby-on-rails

As a user, I can add my favorite book to my book list.
Another user adds the same book to their book list.
There should only be one instance of the book in the database but 2 user/book associations.
class User
has_many :bookReferences
has_many :books, through: :bookReferences
class Book
validates_uniqueness_of :title
has_many :bookReferences
has_many :users, through: :bookReferences
class BookReference
belongs_to :user
belongs_to :book
The first problem, is that if the book is already in the system, the bookReference is not created for the second user because the book isn't unique.
So, if the book is already in the system, I want to just create the association record in the bookReference table.
The second problem, when a user deletes a book, I only want it to delete the reference to the book unless they are the only user referencing that book.
Basically, this is complete overview of the usage I'm trying to achieve:
user1.books.first
=> id: 1, title: "Moby Dick"
user2.books.first
=> id: 1, title: "Moby Dick"
books.all
=> id: 1, title: "Moby Dick"
user1.books.first.destroy
user1.books.first
=> nil
books.all
=> id: 1, title: "Moby Dick"
user2.books.first
=> id: 1, title: "Moby Dick"
user2.books.first.destroy
=> nil
books.all
=> nil
UPDATE
Based on these answers, perhaps I wasn't very clear. Let me try again...
The book controller, has a basic CRUD create method which:
def create
current_user.books.create(name: params[:book][:title])
With the way the has_many through association is setup currently, the book will only be created if it is unique. If it's already in the system, it will return false. What I WANT it to do is create the association with the existing book as if it was a new record. This is what I am currently doing in my application to accomplish this but it feels wrong:
def create
book = Book.where(name: params[:book][:title]).first_or_create!
BookReference.where(book_id: book.id, user_id: current_user.id).first_or_create!
Then the second problem is when a user removes a book from their account. Doing the traditional CRUD destroy will remove it from all accounts:
def destroy
book = current_user.books.find(params[:id])
book.destroy
So to get around this, I'm currently doing the following. Again though, this doesn't feel "right":
def destroy
book = current_user.books.find(params[:id])
# if book was unique to user
if BookReference.where(book_id: book.id).count == 1
# remove book from system
book.destroy
else
# remove book reference but not book
current_user.books.delete(book)
end
end

rails automatically creates the bookreference for you when you use the has_many through association.
has many through creates a "hidden column" called :book_ids in your user model
so in your form that you want to "save" a book to a user you would use
<%= form_for (#user) do |f| %>
.
.
.
<%= f.label :book_ids %>
<%= f.select :book_ids, Book.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %>
your bookreference should be automatically populated and you can display it in the view. Edit and destroys are automatically done for you too. when you delete or edit it will be only the bookreference table.
if you want to know more about associations you can go to
http://api.rubyonrails.org/
and search for ActiveRecord::Associations

I'm still kinda new to rails as well, but this is what I came up with.
book = user1.books.first
if book.users.size > 1
user1.books.first.destroy
else
book.destroy
end
# in order to delete the book's dependency, you will need
class Book
has_many :bookReferences, :dependent => :destroy
Hope this helps.
NOTE:
When you have a model named like BookReference, you refer the model as :book_reference
In your case, you need to change to
class User
has_many :book_references
has_many :books, through: :book_references
class Book
validates_uniqueness_of :title
has_many :book_references
has_many :users, through: :book_references
class BookReference
belongs_to :user
belongs_to :book
UPDATE:
Since you want the new book created when it's not present in the database, one rails method you could take advantage of is Book.find_or_create(). I'm not going into the details but I feel like later on you would want an autocomplete feature to look up the books.
Take a look at this railscast on Autocomplete. Ryan Bates did a great job on setting up the autocomplete and create new entry if it's not found, which is what you are looking for.
Furthermore, if you need an easier way to allow users to add multiple books, here's another railscast that teaches you about Token Fields

Related

Rails many-to-many assignment with unique DB entries

I'm running a Rails 5.2.1 app, with Ruby 2.6.7.
I have a Forum-like app, and I'm trying to implement a home-baked Tagging system, similar to what StackOverflow has. The way I chose to do it, is to have a simple join table that will facilitate a has_many through: relationship for my Tag and Question models.
The problem I'm encountering is that I'm not sure how to assign a Tag to a question without creating a new entry in the Tag table (assuming this is even possible to do). Preferably, I'd want the database to only have 1 entry for each unique tag.
For example, when a new question is created, a user would select a tag which already exists, and the question would be linked to the specific ID of the tag in question.
User creates question, with id: 1, chooses tag that in the DB has id: 1
In the Join table, this would look like question_id: 1 | tag_id: 1
User creates another question, now with id: 2, and again chooses tag id: 1
The table for that has question_id: 2 | tag_id: 1
As it is right now, if I do question.tags.create(id: 1), the tag doesn't get created. On the other hand, if I do question.tags.create(name: "tagname"), the tag does get created, but it's created as a new Tag entry and has id: 2, leaving me with two tags with the same name.
My models:
class Question < ApplicationRecord
has_many :question_tags, dependent: :destroy
has_many :tags, through: :question_tags
end
class Tag < ApplicationRecord
has_many :question_tags, dependent: :destroy
has_many :questions, through: :question_tags
end
class QuestionTag < ApplicationRecord
belongs_to :question
belongs_to :tag
end
How could I achieve the result I want? Presumably, I'd use some sort of before_Save or before_create hook to handle it for me, but I'm unsure of how exactly to implement that properly.
You could find all the Tag ids from the user's selection and then just add those. ActiveRecord gives you #tag_ids. In your service or controller or w/e, you could just add them there; ie select the tags for the question, get the id from the select value, in your controller permit the tag_ids and then assign as needed.
# form
<%= select question[tag_ids][], .... %>
# controller
private
def question_params
params.require(:question).permit(:id, ..., tag_ids: [])
end
# Then used similarly whereever you choose
question = Question.find(params[:id])
question.tag_ids << question_params[:tag_ids]
or
question.tag_ids = question_params[:tag_ids]
You can create/update a question with the tag_ids like any attribute

RoR ActiveRecord Join Help for semi AI App

Working on a RoR app. Think Tinder for art with a little basic AI. As you like or dislike art the app will show you art that you will hopefully like more based on other users’ preferences. I am working on the code to pick the next image to show.
I use the following models:
Basic user info:
class User < ApplicationRecord
has_many :artworks
has_many :artists
has_many :art_views
end
Info about each piece (price, height, width, etc)
class Artwork < ApplicationRecord
belongs_to :user
belongs_to :artist
has_many :art_views
end
To represent which art has been show to a user (boolean liked indicates if the user liked the artwork)
class ArtView < ApplicationRecord
belongs_to :user
belongs_to :artwork
end
The logic is simple: after finding the last Artwork liked by current_user (in ArtView), find other Users who also liked the same Artwork (also in ArtView) then find new Artwork liked by those other Users that has not been seen by the current_user (no record in ArtView)
I am using the following join:
#artwork = Artwork.joins(:artview)
.where(:artviews => { :liked => true})
.where(:artviews => { :artwork_id => #artwork_id})
.where.not(:artviews => { user_id: #id })
I get the following error:
Can't join 'Artwork' to association named 'artview'; perhaps you misspelled it?
Much appreciate any guidance/suggestions (both in fixing the error and/or improving the logic, in general)
An ArtView has many ArtViews, so you have to reference that relationship when using joins.
It should work like this:
Artwork.joins(:art_views)
The same when using the joined columns (no matter the kind of relationship), you should use the "real" name of the table; it's pluralized way - that's how Rails name the tables it creates, unless otherwise specified:
Artwork
.joins(:art_views)
.where(:art_views => { :liked => true})
.where(:art_views => { :artwork_id => #artwork_id})
.where.not(:art_views => { user_id: #id })

Nested form - how to add existing nested object to parent form?

I've got my models setup for a many-to-many relationship:
class Workshop < ActiveRecord::Base
has_many :workshop_students
has_many :students, :through => :student_workshops
accepts_nested_attributes_for :students
end
class Student < ActiveRecord::Base
has_many :student_workshops
has_many :workshops, :through => :student_workshops
accepts_nested_attributes_for :products
end
class StudentWorkshop < ActiveRecord::Base
belongs_to :student
belongs_to :workshop
end
As you can see above, a student can have many workshops and workshop can have many students.
I've looked at the following Rails casts: here and here. And most of the online sources I stumble across only show how to do nested forms for creating new objects within the parent form.
I don't want to create a new object. I only want to add an existing object to the parent form. So for example. If I decide to create a new workshop, I'd like to assign existing students to the workshop.
One thing I don't understand is, how do I link students into the workshop form? Second, when the params are passed, what should be in the controller method for update/create?
If anyone can point me to the right direction, I would appreciate it.
The easiest thing to do is:
<%= f.collection_select(:student_ids, Student.all, :id, :name, {:include_blank => true}, {:selected => #workshop.student_ids, :multiple => true} )%>
You should not have to do anything in the create action.
Ok, for anyone coming across the same issue in the future. The solution I came up with was in def create. I am able to access a POST attribute called student_ids, which comes in the form of an array

Rails 3: Having trouble with nested forms and has_many :through

Here's my model set up.
Band Model
has_many :bands_genres
has_many :genres, :through => :bands_genres
Genre Model
has_many :bands_genres
has_many :bands, :through => :bands_genres
BandsGenre Model
belongs_to :band
belongs_to :genre
I have a form where you can add a new band and then select a genre from a dropdown field that pulls from the pre-set genres in the genre model.
So what I ultimately need to do is set up a form so that when a band adds their band and select a genre, it creates the correct join in the bands_genre model.
Not sure where to start with setting up the form, controllers and models for this.
I'm running Rails 3.0.3
There are quite a few text/video casts covering this, since its a popular use case. I would encourage you to look at:
http://railscasts.com/episodes/73-complex-forms-part-1 or its equivalent asciicast (which is a text based cast of the video).
Further I would recommend you use formtastic. Associations are managed automatically so it makes form building trivial and keeps your code tidy. And yes there are casts for that too.
http://railscasts.com/episodes/184-formtastic-part-1
Edit:
Band Model
has_many :genres, :as => :band_genres
Genre Model
has_many :bands, :as => :band_genres
Your genre table has a band_id, and your band table has a genre_id.
bands_controller
def new
#genres = Genre.all
#post = Post.new
end
posts/new.html.haml
(This part I'm a little unsure of, but it roughly goes like this)
- form_for #post do |f|
= f.select :genre_id, #genres, {}
= f.submit

How to setup and work with RoR Complex Joins?

This code is taken from a previous question, but my question directly relates to it, so I've copied it here:
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class Role < ActiveRecord::Base
has_many :group_memberships
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, :through > :group_memberships
end
New Question Below
How you take the above code one step further and add and work with friends?
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :friends # what comes here?
has_many :actions
end
In the code above I also added actions. Let's say that the system kept track of each user's actions on the site. How would you code this so that each action was unique and sorted with the most recent at the top for all the user's friends?
<% for action in #user.friends.actions %>
<%= action.whatever %>
<% end %>
The code above may not be valid, you could do something like what I have below, but then the actions wouldn't be sorted.
<% for friend in #user.friends %>
<% for action in friend.actions %>
<%= action.whatever %>
<% end %>
<% end %>
UPDATE
I guess the real issue here is how to define friends? Do I create a new model or join table that links users to other users? Ideally, I'd like to define friends through the common group memberships of other users, but I'm not sure how to go about defining that.
has_many :friends, :through => :group_memberships, :source => :user
But that doesn't work. Any ideas or best practice suggestions?
It's been a while since I've worked with Rails (think Rails 1.x), but I think you can do something like the following:
Action.find(:all, :conditions => ['user_id in (?)', #user.friends.map(&:id)], :order => 'actions.date DESC')
and then in your view:
<% #actions.each do |action| %>
<%= action.whatever -%>
<% end %>
To add friends you can use has_and_belongs_to_many or has_many :through. Here is example how to do it. You should self join users table.
To list recent actions, get them from db using :order => :created_at (or :updated_at). Than you can filter out actions that not belongs to users.
#actions = Action.all(:order => :created_at).select {|a| a.user.friends.include?(#user)}
Probably you can also write sql query to do this.
You could make a "friend" object that uses the "user" table and go from there or you could
define a friend as a partial entity (friend name and user_id reference) and pull the user object into the friend object.
I like this last model better because it allows you to represent "deleted" friends more easily. You would still have a copy of the friend name (even if it is a duplicate of the user name) and the ID to identify if the user is gone or not. Keeps the "friend" concept a little further away from the "user" concept and any security implications. I will argue it's appropriate de-normalization to duplicate the "name" field as it adds flexibility and "shadow" information about a user that used to exist.
My take on it, though. You would still have to eventually self-join back to the user table, but perhaps not as much. If you REALLY hate to de-normalize anything you can have a Person object that Friend and User both refer to for information. Personally, I think that is a nice theoretical construction, but not worth the complexity it would introduce into the bulk of your code (80% of it being talking about the user and 20% talking about the friend). I would make the user easy to talk about and the friend a little hard... not both. :)
Hope this helps. I enjoyed mentally toying with this one. :)

Resources