Validate uniqueness through join model in rails - ruby-on-rails

I have a has_many :through association setup between two tables (Post and Category). The reason I'm using has_many :through instead of HABTM is that I want to do some validation on the join table (PostCategory).
So I have 4 models in use here:
User:
has_many :posts
has_many :categories
Post:
belongs_to :user
has_many :post_categories
has_many :categories, :through => :post_categories
Category:
belongs_to :user
has_many :post_categories
has_many :posts, :through => :post_categories
PostCategory:
belongs_to :post
belongs_to :category
Basically what I want is: Users can create posts, users can also create their own categories. A user can then categorize posts (not just their posts, any posts). A post can be categorized by many different users (in different ways potentially), and a category could contain many different posts (A user could categorize N posts under a specific category of theirs).
Here's where it gets a little bit tricky for me (I'm a Rails noob).
A post can ONLY belong to ONE category for a given user. That is, a post CANNOT belong to more than ONE category for any user.
What I want to be able to do is create a validation for this. I haven't been able to figure out how.
I've tried things like (inside PostCategory)
validates_uniqueness_of :post_id, :scope => :category_id
But I realize this isn't correct. This would just make sure that a post belongs to 1 category, which means that after one user categorizes the post, no other user could.
Really what I'm looking for is how to validate this in my PostCategory model (or anywhere else for that matter). I'm also not against changing my db schema if that would make things easier (I just felt that this schema was pretty straight forward).
Any ideas?

The simpliest way is to add user_id to PostCategory and to validate uniqueness of post_id with user_id scope.
Another way is to create custom validation which checks using sql if category owner has added category to that post.

Option 1 : use a before_save. In it, do a SQL look up to make sure a post with a similar category for your user doesn't exist (take care that on edit, you'll have to make sure you don't look-up for the current Post that is already in the DB)
Option 2 : custom validators :
http://guides.rubyonrails.org/v3.2.13/active_record_validations_callbacks.html#custom-validators
Never used them, but sounds like it can do what you want

Related

Custom scope on has_many, :through association (Rails 4)

CONTEXT:
In my setup Users have many Communities through CommunityUser, and Communities have many Posts through CommunityPost. If follows then, that Users have many Posts through Communities.
User.rb
has_many :community_users
has_many :communities, through: :community_users
has_many :posts, through: :communities
Given the above User.rb, Calling "current_user.posts" returns posts with one or more communities in common with current_user.
QUESTION:
I'm looking to refine that association so that calling "current_user.posts" returns only those posts whose communities are a complete subset of the current_user's communities.
So, given a current_user with community_ids of [1,2,3], calling "current_user.posts" would yield only those posts whose "community_ids" array is either 1, [2], [3], [1,2], [1,3], [2,3], or [1,2,3].
I've been researching scopes here, but can't seem to pinpoint how to accomplish this successfully...
Nice question...
My immediate thoughts:
--
ActiveRecord Association Extension
These basically allow you to create a series of methods for associations, allowing you to determine specific criteria, like this:
#app/models/user.rb
has_many :communities, through: :community_users
has_many :posts, through: :communities do
def in_community
where("community_ids IN (?)", user.community_ids)
end
end
--
Conditional Association
You could use conditions in your association, like so:
#app/models/user.rb
has_many :posts, -> { where("id IN (?)", user.community_ids) }, through: :communities #-> I believe the model object (I.E user) is available in the association
--
Source
I originally thought this would be your best bet, but looking at it more deeply, I think it's only if you want to use a different association name
Specifies the source association name used by has_many :through
queries. Only use it if the name cannot be inferred from the
association. has_many :subscribers, through: :subscriptions will look
for either :subscribers or :subscriber on Subscription, unless a
:source is given.
Being honest, this is one of those questions which needs some thought
Firstly, how are you storing / calling the community_ids array? Is it stored in the db directly, or is it accessed through the ActiveRecord method Model.association_ids?
Looking forward to helping you further!
You don't show the model and relationship definitions for for Community or CommunityPost so make sure you have a has_many :community_posts and a has_many :posts, through: :community_posts on your Community model and a belongs_to :community and a belongs_to :post on CommunityPost. If you don't need to track anything on ComunityPost you could just use a has_and_belongs_to_many :posts on Community and a join table communities_posts containing just the foreign keys community_id and post_id.
Assuming you have the relationships setup as I describe you should be able to just use current_user.posts to get a relation that can be further chained and which returns all posts for all communities the user is associated with when you call .all on it. Any class methods (such as scope methods) defined your Post model will also be callable from that relation so that is where you should put your refinements unless those refinements pertain to the Community or CommunityPost in which case you would put them on the Community or CommunityPost models respectively. Its actually rare to need an AR relationship extension for scopes since usually you also want to be able to refine the model independently of whatever related model you may use to get to it.

What is 'the Rails way' to have forms not directly mapped to an object model?

Let's say I have three tables to accommodate a many-to-many relationship.
class User < ActiveRecord::Base
has_many :user_hobbies
has_many :hobbies, :through => :user_hobbies
end
class UserHobbies < ActiveRecord::Base
belongs_to :user
belongs_to :hobby
end
class Hobby < ActiveRecord::Base
has_many :user_hobbies
has_many :users, :through => :user_hobbies
end
And I wanted to have a form in which a user can input as many hobbies as they want, where each would be stored into the correct tables (in my situation, the 'Hobby' table is preset, the user may select from enumerated values, not add them)
How would I go about producing a form to achieve this? I would use JavaScript for Auto-Completion and dynamic fields (i.e. each time you enter a field, another appears).
In order to produce this form there are two options which are available to you..
a. The first option is to use the virtual attribute which would accept the comma separated values of the hobbies of the user and then when you save the user the virtual attribute would then set the hobbies corresponding to the respective user..See this railcast
http://railscasts.com/episodes/16-virtual-attributes
http://railscasts.com/episodes/167-more-on-virtual-attributes
b. The second option is to use the accepts_nested_attributes_for which would automatically include the attributes for the nested relationships in the parent model...Consult the API for more information on accepts_nested_attributes_for
Also I can see a bug in the above definitions as there should be has_many :users :through :user_hobbies relationship in the Hobbies table becoz we might want to know the user having the..
As far as the auto-completion is concerned, there is a gem called as auto-complete-rails and its documentation is pretty simple and standard to use and understand..

Whats the cleanest way to handle many-to-many relationships in ruby on rails?

I have one model say user, that can live in multiple towns (represented as another model). If I create a new user I have to choose (and edit) the different towns that they live in. Due to time constraints, I often end up with a "hackyier than I would like" solution involving something like: http://blog.hasmanythrough.com/2006/4/20/many-to-many-dance-off.
Any nice solutions that are popular with SO?
cheers...
Slothishtype
The has_and_belongs_to_many association was built for this very situation. Here is the documentation on it: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
Otherwise, if you need to store information abotu the association itself (fields that would not exist in the city table or the user table, but in between), you might just want to set up two, parallel 'has_many_through' associations, and set up a seperate 'user_city' table. So it would be in the user table
has_many :user_cities
has_many :cities, :through => :user_cities
and in the cities table
has_many :user_cities
has_many :users, :through => :user_cities
Then, you CAN just call: user.cities, and get a list of the cities the user lives in.

Help with rails content filtering

Im creating my own blog managing app in rails (for experimental purposes).... What would the best way to get this done?
I have posts and categories.
I want to have a dropdown of categories for the user to select one when they create a new post.
Now, each user will have different privileges so not all categories should appear for all users....
Right now Im at the point where I can create posts and choose which category I want... I havent added any filter per user support....
please help me on where should I go now??
First you will need to implement authentication and authorization. There are many good Rails tutorials on these subjects so I won't go into more detail here.
At this point you will have models for User, Post, and Category. You need a list per-user of authorized categories. A naive model:
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
But that is misleading, because a user isn't actually "in" any categories. You probably want something like a join model like so:
class UserCategoryAuthorization < ActiveRecord::Base
belongs_to :user
belongs_to :category
// More fields here; possibly:
// belongs_to :authorized_by, :class_name => 'User'
end
class User < ActiveRecord::Base
has_many :user_category_authorizations
has_many :authorized_categories,
:through => :user_category_authorizations,
:source => :category
end
To start with I would give Users a has_many categories relationship(Which you could turn into its own model object at some point if this idea gets more complicated..or now if it already makes sense) and then assuming you already have log in functionality you can ask the logged in user for its categories and populate the drop down appropriately.
If this is a security issue rather than just convenience then you will need to validate the chosen category is in the users categories when the form is submitted back to the server.
If you don't already have logins I believe there are several rails plug-ins that attempt to help you get this functionality quickly.

Rails has_one :through association

Rails has a has_one :through association that helps set up a one-to-one association with a third model by going through a second model. What is the real use of that besides making a shortcut association, that would otherwise be an extra step away.
Taking this example from the Rails guide:
class Supplier < ActiveRecord::Base
has_one :account
has_one :account_history, :through => :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ActiveRecord::Base
belongs_to :account
end
might allow us to do something like:
supplier.account_history
which would otherwise be reached as:
supplier.account.history
If it's only for simpler access then technically there could be a one-to-one association that connects a model with some nth model going through n-1 models for easier access. Is there anything else to it that I am missing besides the shortcut?
Logic, OK it might sound a bit weak for this but it would be logical to say that "I have a supplier who has an account with me, I want to see the entire account history of this supplier", so it makes sense for me to be able to access account history from supplier directly.
Efficiency, this for me is the main reason I would use :through, simply because this issues a join statement rather than calling supplier, and then account, and then account_history. noticed the number of database calls?
using :through, 1 call to get the supplier, 1 call to get account_history (rails automatically uses :join to retrieve through account)
using normal association, 1 call to get supplier, 1 call to get account, and 1 call to get account_history
That's what I think =) hope it helps!
I'm surprised no one has touched on Association Objects.
A has_many (or has_one) :through relationship facilitates the use of the association object pattern which is when you have two things related to each other, and that relation itself has attributes (ie a date when the association was made or when it expires).
This is considered by some to be a good alternative to the has_and_belongs_to_many ActiveRecord helper. The reasoning behind this is that it is very likely that you will need to change the nature of the association or add to it, and when you are a couple months into a project, this can be very painful if the relationship were initially set up as a has_and_belongs_to_many (the second link goes into some detail). If it is set up initially using a has_many :through relationship, then a couple months into the project it's easy to rename the join model or add attributes to it, making it easier for devs to respond to changing requirements. Plan for change.
Inverse association: consider the classic situation user-membership-group. If a user can be a member in many groups, then a group has many members or users, and a user has many groups. But if the user can only be a member in one group, the group still has many members: class User has_one :group, :through => :membership but class Group has_many :members, :through => memberships. The intermediate model membership is useful to keep track of the inverse relationship.
Expandability: a has_one :through relationship can easy be expanded and extended to a has_many :through relationship

Resources