More than one belongs_to - ruby-on-rails

I’m new to Rails and I’ve been reading Michael Hartl’s tutorial and I am not sure if I’m doing this correctly. I have Users, Posts, and Categories:
Users can create Posts and Categories
A Post can be assigned to only one Category.
Currently, when the User creates a post they type in the Category (let’s just say the Category will always exist in the database) and from there it looks up the ID of the Category and passes that along to the post creation. This is what I’m running to create the post and assign it to a Category in my Post_Controller:
category_id = Category.find_by_name(post_params[:category])
#post = current_user.posts.build(title: post_params[:title], content: post_params[:content], category_id: category.id)
My question is: Is this the proper way to enter data with two belong_to’s? I’ve dug around and I can’t find a simple answer to this. To me it seems that passing a category ID like this is not secure but I don’t know of another way to do this. Here’s my basic Model information (just the belong_to’s, has_many, etc). Please let me know if you need more:
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation
has_secure_password
has_many :posts
has_many :categories
class Post < ActiveRecord::Base
attr_accessible :title, :content, :category_id
belongs_to :user
belongs_to :category
class Category < ActiveRecord::Base
attr_accessible :name
belongs_to :users
has_many :posts
validates :name, presence: true, uniqueness: true, length: {maximum:30}
validates :user_id, presence: true

Is this the proper way to enter data with two belong_to’s?
It's fine. Does it work? If it works, it's fine. Maybe there are things you could do to tighten it up later, if you find you're making the Category.find... call a lot, but, you're also just starting, so don't worry about stuff like that too much.
To me it seems that passing a category ID like this is not secure but I don’t know of another way to do this.
Again, don't worry about that too much at the moment. If you want to read up on Rails security, though, take a look at this.

If all the information you need to create a new post is in post_params, it seems you're doing that Category.find_by_name unnecessarily. You should be collecting the category_id in your params rather than the category's name.
And then in your PostsController:
#post = current_user.posts.build(post_params)
Just remember to allow :category_id along with the other regular attributes in your post_params and you'll be golden.

Related

Can't figure out how to update a model with new tables?

I want to update my user Model with new tables, The tables being the following.
I need the user to be able to choose a location upon signing up.
I also need the users to be able to select up to 3 genres of music they'd prefer to listen to while signing up.
Now, The problem is I am going to let users select which genre they like, I also want them to be able to click on the genre link to see what other users on the website also enjoy that type of genre. So would that mean the genre would have to be a model while the location should be a table?
This is what I have in my user.rb,
class User < ActiveRecord::Base
has_secure_password
before_save { self.email = email.downcase }
validates :first_name, :last_name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :first_name, :last_name, presence: true, uniqueness: true
validates_inclusion_of :age, in: 10..100
validates :password, presence: true, length: { minimum: 4 }, allow_nil: true
has_many :posts
has_attached_file :profile_picture, :styles => { :medium => "300x300>", :thumb => "100x100>" },
:default_url => "app/assets/images/missing.png",
:path => ":rails_root/public/system/:class/:attachment/:id_partition/:style/:filename"
validates_attachment_content_type :profile_picture, :content_type => /\Aimage\/.*\Z/
def self.search(query)
where("email like ?", "%#{query}%")
end
end
If you guys need any other code just let me know, And thanks in advance!
If you're going to ask for help the least you could do is clean up the spacing and indentation. Group all of your validations together, etc.
As for how you want to setup your sign in process it looks like you're building your own. Feel free to post all files related to your sign in process but it'd be a lot easier to use devise and customize it's views (click for tutorial). You can have your users add their location & genre preferences here.
Before I go any further let's get on the same page for definitions. I think you mean columns instead of tables in your question above but let me know if I misunderstood you.
Moving along: you have a number of ways you can setup your genres. I'd recommend creating a new genre table (not column) and associating that with your users through a 'through' table (click for solution) I know this link is old but it'll set you on the right direction. This might be a lot for you to start with especially if you haven't done associations before but give it a try.
If all else fails you can reduce the number of features to start with and ask your users if they'd want that feature. Good luck!
You'd better to make new Genre model with genres as table. When user want to select its location you can add has_one association when user only choose one location.
genre.rb
attributes: id, name, decription
class Genre < ActiveRecord::Base
has_many: genres_users
has_many: users, through: genres_users
end
genres_user.rb
attributes: id, user_id, genre_id
class GenresUser < ActiveRecord::Base
belongs_to :user
belongs_to :genre
end
user.rb
attributes: your attribute field
class User < ActiveRecord::Base
has_many: genres_users
has_many: genres, through: genres_users
has_one: location
end
location.rb
class Location < ActiveRecord::Base
belongs_to :user
end
For details of generating migration and association basics
I want to update my user Model with new tables
As mentioned in other answers, each Model should represent a single table.
This is not always the case; STI models will not have tables, and you can use a model as you would a class (without DB integration).
However, the point is that you cannot just "add" tables to a model. You can add associations, which is a totally different concept. I'll explain it here:
ORM
To better understand what you're asking, it will help knowing that Rails is an abstraction layer for relational database & web server software. This means that when you deal with data, you're not pulling from a database, but dealing with objects which can be populated in various ways.
Rails is an MVC (Model View Controller) framework:
This means that your "data" is not tied to tables etc, it's bound to models which are meant to provide you with objects that you can use within your application's business logic.
In other words, you shouldn't be thinking of your Rails app as a way to access data, but to manipulate objects.
This is done with the ORM called ActiveRecord. ActiveRecord is the thing responsible for when you call the likes of User.find, and is behind the Rails' ActiveRecord associations.
What you're really asking is "How do I add associations to my User model?"
--
I need the user to be able to choose a location upon signing up.
I also need the users to be able to select up to 3 genres of music they'd prefer to listen to while signing up.
Here's what I'd do:
#app/models/user.rb
class User < ActiveRecord::Base
# columns id | user | etc | location_id | created_at | updated_at
belongs_to :location
has_and_belongs_to_many :genres
end
#app/models/location.rb
class Location < ActiveRecord::Base
# columns id | etc | etc | created_at | updated_at
has_many :users
end
#app/models/genre.rb
class Genre < ActiveRecord::Base
has_and_belongs_to_many :users
end
Now, there is another answer which mentions having a model called genre_user.rb. This is for has_many :through, and I don't believe is applicable in this case.
I'd recommend using a has_and_belongs_to_many join table, and simply validate the number of genres to 3 (I'll let you work that out):
To do this, you need a table called genres_users with the attributes genre_id and user_id. This will give you the ability to call:
$ #user = User.find 1
$ #user.genres #-> 1,2,6

Creating new object with relations without the related id

I have a rails app with the following models:
class Product < ActiveRecord::Base
has_many :stores, through: :product_store
attr_accessible :name, :global_uuid
end
class ProductStore < ActiveRecord::Base
attr_accessible :deleted, :product_id, :store_id, :global_uuid
belongs_to :product
belongs_to :store
end
Since this model is for a REST API of a mobile app, I create the objets remotely, on the devices, and then sync with this model. As this happens, it may happen that I have to create a ProductStore before having an id set for Product. I know I could batch the API requests and find some workaround, but I've settled to have a global_uuid attribute that gets created in the mobile app and synced.
What I'd like to know is how can I make this code in my controller:
def create
#product_store = ProductStore.new(params[:product_store])
...
end
be aware that it will be receiving a product_global_uuid parameter instead of a product_id parameter and have it properly populate the model.
I figure I can override ProductStore#new but I'm not sure if there's any ramification when doing that.
Overriding .new is a dangerous business, you don't want to get involved in doing that. I would just go with:
class ProductStore < ActiveRecord::Base
attr_accessible :product_global_uuid
attr_accessor :product_global_uuid
belongs_to :product
before_validation :attach_product_using_global_uuid, on: :create
private
def attach_product_using_global_uuid
self.product = Product.find_by_global_uuid! #product_global_uuid
end
end
Having these kinds of artificial attr_accessors that are only used in model creation is kind of messy, and you want to avoid passing in anything that isn't a direct attribute of the model you are creating where possible. But as you say there are various considerations to balance, and it's not the worst thing in the world.

How to automatically copy ActiveRecord's field value to its newly created relation?

I have following classes:
class User < ActiveRecord::Base
attr_accessible :language_id
has_many :lists
end
class List < ActiveRecord::Base
attr_accessible :language_id
belongs_to :user
end
I would like to automatically assign user's language_id to the newly created list:
# list.language_id should be equal to user.language_id
list = user.lists.create
The cleanest solution I found is adding a filter into the List class:
before_validation :assign_language_id, on: :create
def assign_language_id
self.language_id = self.user.language_id
end
It doesn't seem to be a bad solution, but I would like to know if Rails have some magic to handle that prettier.
I know that I can always ask for list.user.language_id instead of copying it, but please assume, that I really need to have it here.
Thank you for your answers.
Two things:
Can you have a list without user? if yes than that callback will fail, you need to check if user exists.
is list language_id always same as user.language_id? If yes I would recommend delegating language_id and language to user using delegate :language, :language_id, :to => :user you can look at http://apidock.com/rails/Module/delegate to see how delegate works and some more options.

Nested form and has_many :through

I have a little sample app where there are 3 models: Members, Groups and Subscriptions. The idea is that member can subscribe to groups.
class Member < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :groups, through: :subscriptions
attr_accessible :email
validates :email, presence: true
end
class Group < ActiveRecord::Base
has_many :subscriptions, dependent: :delete_all
has_many :members, through: :subscriptions
accepts_nested_attributes_for :subscriptions
attr_accessible :name, :subscriptions_attributes
validates :name, presence: true, uniqueness: true
end
class Subscription < ActiveRecord::Base
belongs_to :group
belongs_to :member
attr_accessible :group_id, :introduction
validates :group_id, presence: true
validates :introduction, presence: true
end
I'm trying to create a form for new groups, and nest the introduction attribute inside.
My controller methods:
def new
#group = Group.new
#group.subscriptions.build
end
def create
#member = Member.first
#group = #member.groups.build(params[:group])
if #group.save
flash[:success] = "Saved"
redirect_to group_path(#group)
else
render :new
end
end
But it does not work. It throws the error group_id can't be blank. So I don't know how to assign the new group to the subscription.
Also, the member_id is being created as nil. But as you can see, I'm creating the group from the #member variable, so I think it should be initialized, but it does not.
Anyone can show me the light?
You can see the sample app here: https://github.com/idavemm/nested_form
Make sure all the attributes you're trying to assign are attr_accessible. You may just disable it and see if it works, or see at the warnings in the Rails server log.
Update: you should add accepts_nested_attributes_for to the Member model and use a multimodel form with fields_for.
I think you're thinking about your models in the wrong way. Will each member have a different introduction for each group. So, for example, will member1 have one introduction and member2 have a different introduction?
The Subscriptions model should store information about the relationship between and member and group. In that case, introduction would be better to have in the group model. The reason you are getting an error is because you are trying to create a subscription(when you set the introduction attribute) for a group that hasn't been made yet.
So, move introduction to the group model and then, if you want the creator of a group to be automatically subscribed to it (which you should), add the code to create a subscription to the controller in the create action after the record is saved. Then, on the subscription model, you can do cool things like having a state machine that tracks a member's status with the group (moderator, newbie, veteran member, etc).
After many hours of investigation and frustration, I reported it to Rails devs and I finally have a solution:
Rails 3 is unable to initialize group_id and member_id automatically in the way it is defined in the question.
So, for now, there are two ways to make it work:
Add the member_id as a hidden field in the view.
Change everything so is the Subscription model who has accepts_nested_attributes_for. That way, the new object to be created is the Subscription, and Group will be the nested model.
The first option has an important security hole, so I don't recommend it.
The second option, although not much logical, is the cleaner and supposedly the "Rails way" to fix this problem.
I just ran into the same issue, the solution was to remove the presence validation from the related model (based on this question), in your case, remove:
validates :group_id, presence: true
Once that validation it's gone, everything runs like clockwork

Rails: Validating existence of an association

I have a Category and a Post model, with each Post belonging to a Category. Before creating or updating a post, I need to check that the category selected exists. What's the best way to validate this information?
At the moment, I'm doing a find in the controller to ensure that the category exists. Is it possible to put these kinds of validations in the model?
http://blog.hasmanythrough.com/2007/7/14/validate-your-existence
class Post < ActiveRecord::Base
belongs_to :category
validates_presence_of :category
end
-OR-
class Post < ActiveRecord::Base
belongs_to :category
validates :category, presence: true
end
Rails versions prior to 3.2:
class Post < ActiveRecord::Base
belongs_to :category
validates_existence_of :category
end
In Rails 3.2, validates_existence_of is replaced by validates_presence_of.
I've put this in my model:
validate :ensure_category_exists
def ensure_category_exists
errors.add('Category') unless self.blog.categories.find_by_id(self.category_id)
end
Which prints "Category is invalid" if the category does not exist for the parent blog.
It's definitely worth mentioning my experiences. This is for Rails 4 (potentially other versions as well).
Given an entity has_many or has_one of a model.
Validation that will ensure the entered association (association ID) exists, even if there is an ID given in the submission.
validates_presence_of :model
IS NOT THE SAME as a validation that will ensure there is something entered (not blank) in the input.
validates_presence_of :model_id
You may be able to get by with just the former, but I have both to have more specific error messages.
In my way of thinking a better choice is this gem: https://github.com/perfectline/validates_existence
It validates the related model's existence in the database. Imagine you have a dropdown field that gives back some garbage data even when you do not select anything (default non selected first field label as value). Validating presence won't work, as it will pass for existing data. But we want some kind of a constraint and this DB side check is what solves the problem.
In rails 5 and above, belongs_to automatically validates for presence.
But if you use belongs_to :category, optional: true it does not validate presence, and you can then do post.update!(category: -1) which is not great. To fix that:
validates :category, presence: true, if: :category_id
Just to be clear, the above is useful only when the association is optional.
In Rails 3, validates_associated is probably what you're looking for?
http://guides.rubyonrails.org/active_record_validations_callbacks.html#validates_associated

Resources