Managing Multiple Hierarchical Rails Models Interlinked to Multiple Levels - ruby-on-rails

I am building a To Do application but with multiple levels of tasks and subtasks. I have been following Ruby on Rails Tutorial by Michael Hartl and almost everything I have learnt till now is from there.
My app has multiple hierarchy models. What I mean is that at the top is the User, a user can have multiple goals, each goal can have multiple tasks, and each task can have multiple subtasks.
Just to give you a more clear idea, I'll list down the hierarchy
User
Goals (multiple goals per user)
Tasks (multiple tasks per goal)
Subtasks (multiple subtasks per task)
Now, when I created the models, I included a dependency (has_many/belongs_to) between goals and users, but I also did the same for tasks and users and subtasks and users. The reason for this is that I wanted an 'id' column in all tables that corresponds to the user table so that I can list down all subtasks/tasks/goals of a user very easily.
Coming to the front end, I need forms that are capable of adding goals/tasks/subtasks.
The relation between users and goals is not problematic at all. The has_many/belongs_to relation allows me to user something like
def create
#goal = current_user.goals.build(path_params)
if #goal.save
redirect_to current_user
else
.
.
end
end
However, the problem arises when I have 3 levels of hierarchy. User, goals and tasks.
My question is that how exactly do I do this for the task controller?
I tried using something like
#task = current_user.goals.tasks.build(task_params)
but that has the obvious flaw apart from probably being syntactically incorrect : there is no way to detect what particular goal this task is being assigned to.
What I can think of is that the front end form must contain a field (perhaps hidden) that contains the goal ID to which the task is being assigned. But not entirely sure is this is correct as well or how if a front end gimmick like that can actually help in case there are even more levels of hierarchy.
Please tell me where I am going wrong / whats a better way to do this.
I am only a beginner right now so I am very confused on how multiple level model hierarchies are managed.

What I can think of is that the front end form must contain a field (perhaps hidden) that contains the goal ID to which the task is being assigned.
More or less. A better way is to have RESTful resources here. So you'll have this structure of nested resources:
# routes.rb
resources :users
resources :goals do
resources :tasks do
resources :subtasks
end
end
This means that to create a task, you have to send a POST request to
/goals/1/tasks
This way, in your TasksController, you'll have params[:goal_id] and now you can do something like this:
class TasksController
before_action :load_goal
def create
#task = #goal.tasks.build(task_params)
...
end
private
def load_goal
#goal ||= Goal.where(id: params[:goal_id]).first
end
end
Repeat for other levels of hierarchy.
As for the propagating user_id down the line, you can do something like this:
class Task
belongs_to :goal
before_save :copy_user_id_from_parent
def copy_user_id_from_parent
self.user_id = goal.user_id
end
end

Related

Is it better to update an associated record through the parent or by itself?

Imagine we have an Article and a Comment model. We setup our routes like:
# routes.rb
resources :articles do
resources :comments
end
Now, we can destroy a comment via the CommentController, but there are a number of approaches that I've seen been implemented.
# method 1
def destroy
Comment.where(article_id: params[:article_id]).find(params[:id]).destroy
end
# method 2
def destroy
Comment.find(params[:id]).destroy
end
# method 3
def destroy
article = Article.find(params[:article_id])
comment = article.comments.find(params[:id])
comment.destroy
end
Which is better and why?
I've seen in old Railscasts episodes and blogs that we should do the former for "security" reasons or because it is better to ensure that comments can only be found within their respective article, but why is that better? I haven't been able to find anything that goes too deep into the answer.
When you are working with nested data in this way, it's better to scope your model lookup under the parent to avoid people iterating through ids in a simplistic way. You generally want to discourage that, and it will protect you against more serious security problems if it's a habit.
For instance, say you have some sort of visibility permission on Article. With method 2, it is possible to use an article_id that you are allowed to see to access Comments that you aren't.
Methods 1 & 3 are ostensibly doing the same thing, but I would prefer 1 because it uses fewer trips to the DB.

How to join on nested belongs_to in Ruby on Rails

I have three models, Application, which belongs to a Board, which belongs to a User.
class User < ActiveRecord::Base
has_many :boards
end
class Board < ActiveRecord::Base
belongs_to :user
has_many :applications
end
class Application < ActiveRecord::Base
belongs_to :board
end
I'm always only ever going to want to show the boards or applications for the current user. How can I say "show every application for the current board for the current user"? Basically how to query for something for specific parent id values.
You should provide the current user id and board id at first.
user = User.find(user_id) #get current user
board = user.boards.find(board_id) #get current board
board.applications #get every application
You can get more info from Rails Guide--Active Record Associations
show every application for the current board for current_user
The power of ActiveRecord should make this relatively simple:
board = current_user.boards.find params[:board_id]
board.applications #-> collection of Application objects for board
This assumes you're using devise, and thus have access to the current_user method.
If not, you'll be able to use something like the following:
#config/routes.rb
resources :users do
resources :applications #-> url.com/users/:user_id/applications
end
#app/controllers/applications_controller.rb
class ApplicationsController < ApplicationController
def index
#user = User.find params[:user_id]
#boards = #user.boards
end
end
#app/views/applications/index.html.erb
<% #user.boards.each do |board| %>
<%= board.name %>
<% board.applications.each do |application| %>
<%= application.name %>
<% end %>
<% end %>
<% end %>
ORM
I'll give you some context to this (hopefully it will help you in the future).
Firstly, you must understand that Rails is built on top of a relational database - or at least it should be. Relational databases use foreign_keys to give you access to associative data objects:
Whilst this might not seem important, it explains the entire functionality of the ActiveRecord system.
ActiveRecord is what's known as an ORM (Object Relationship Mapper)... in short, gives you the ability to populate objects with associative data from your relational database.
In the case of Ruby/Rails, the object orientated nature of ActiveRecord is a direct result of Ruby. That is, Ruby is an object orientated language; everything you do in it revolves around objects:
With Rails, those objects are built in the models.
Thus, if you want to understand how Rails works - you really need to get down to the model level -- you have to understand that calling Model.find x will build an object around that model, populating it with data from your database.
It's very clever.
The associations are provided by ActiveRecord, and pulled through the relational database infrastructure. This means that you can call the likes of #board = #user.boards.first and populate with the correct data.
Here's a good demonstration of how it works:
I'd share about how running query with ActiveRecord::Relation. To know about query, no matter you want to show data that far away from the current table but when those have associations, those could be connected.
Step by step to do query:
Determine all relations tables
Here, you have to determine tables that related to its table associations. In this case: users, boards, and applications.
Determine the condition
You can put current_user is a condition. You need users, boards, applications
So the condition is:
User.joins(boards: :applications).where("users.id = ?", current_user.id)
NOTE:
I would try to explain. User joins boards because user has many boards. Next boards has many applications so we have to join boards with application into boards: :applications.
This is good explain for query has many through associations. activerecord-query-through-multiple-joins

Rails: MVC How to use custom actions

I have a table output from entries using the rails generated scaffold: CRUD ops.
If I want to make another action on the table like the default "Show, Edit, Destory" like a library book "check in", that will update the status to "checked in"...
What would be the proper way to use the model and controller to update? (Using mongodb)
Better stated: What's the best way to have many custom actions? Think of it like many multi purpose "Facebook Likes".
On the table, list of actions "Punch this", "Check out this"...
There are lots of ways to handle this, but I typically like to isolate actions like this in their own controller action with it's own route.
Model
To keep things tidy I recommend adding a method to the model that updates the attribute you are concerned about. If you aren't concerned with validation you can use update_attribute. This method skips validations and saves to the database
class LibraryBook < ActiveRecord::Base
def check_in!
self.update_attribute(:checked_in, true)
end
end
View
You'll need to update the index.html.erb view to add the link to update the individual record. This will also require adding a route. Since you are updating the record you will want to use the PUT HTTP verb.
routes.rb
resources :library_books do
match :check_in, on: :member, via: :put # creates a route called check_in_library_book
end
index.html.erb
Add the link
link_to check_in_library_book_path(library_book), method: :put
Controller
Now you need to add the action within the controller that calls the #check_in! method.
class LibraryBooksController < ApplicationController
def check_in
#library_book = LibraryBook.find(params[:id])
if #library_book.check_in!
# Handle the success
else
# Handle the Failure
end
end
end
In my opinion, the best way to handle status workflows like this is to think about it in terms of events, and then just think of status as most recent event. I usually create an event_type table with a name and code (so, e.g. Check In and CHECK_IN for name and code, respectively), and then an event table with an event_type_id, timestamp, and usually some kind of user id, or IP address, or both.
Then you could say something like this:
class Thing < ActiveRecord::Base
has_many :events
def status
events.order("created_at DESC").first.event_type.name
end
end
There are also "audit trail" gems out there, but in my (limited) experience they aren't very good.
This doesn't speak to MongoDB, and may in fact be incompatible with Mongo, but hopefully it at least points you in the right direction or gives you some ideas.

Limiting number of rows of a table in rails_admin

I have a Ruby on Rails application that uses rails_admin (https://github.com/sferik/rails_admin) as the backend.
I have a model called banner, so a table in the database called banners. The admin can create as many banners as he can and can also delete them. But I want to fix the number of banners in 3. I want to have 3 banners (already created) and I want the admin cannot create nor destroy any banners.
Could someone help me?
Thanks!
class Thing < ActiveRecord::Base
has_many :banners
end
app/controllers/things_controller.rb
def create
#thing = Thing.new
#thing.banners << Banner.new(:name=>'Banner 1',....)
#thing.banners << Banner.new(:name=>'Banner 2',....)
#thing.banners << Banner.new(:name=>'Banner 3',....)
#thing.save
end
Now so long as nowhere else to you call #thing.banners << , you are guaranteed that any thing will always have three banners.
Validations to the rescue:
class Thing < Active Record::Base
has_many :banners
validate :banner_count
private
def banner_count
errors.add(:base, "Banner count must be 3") unless self.banners.count == 3
end
end
#RadBrad makes the point that you could use has_many on another model that represents the set of three banners. Maybe could call it BannerSet or similar. You could either create the three at once like he said, or in the BannerSet validation, you could ensure there are only 3 banners associated.
You could also have even 3 attributes (columns) on the BannerSet model that would have the 3 ids of the banners. If you are sure it will always be 3 banners, then that may be a fine design also.
But, here is how you'd do it if you just had a controller for Banner, which wouldn't be the best way, as you'll see.
First, you could possibly use declarative authorization in the controller:
authorization do
has_permission_on :banners, :to => [:new, :create] do
Banner.count < 3
end
end
To ensure that you still can't add a banner even if it was added after you got to the create screen to add it, also add a validation to the Banner model:
before_create :validate_max_banners
def validate_max_banners
errors.add_to_base("Only 3 banners are allowed.") if Banner.count == 3
errors.empty?
end
Neither will totally ensure you can only have 3 rows in that table though. To do that, you'd need a trigger or similar on the DB side, as described in this q&a. But, for a basic solution, that might be fine.
Note that even though RailsAdmin can be configured and customized pretty easily (see its wiki for documentation), I'd also consider using ActiveAdmin, if you need a lot more customization.

Proper way to have a resource available by both association and by itself

First, I apologize for the poor title - I'm trying to think of a better way to explain it in a few words for that, but can't think of one.
Basically, as an example (note the model names are just examples; I know the actual concept of tasks being associated to many projects doesn't make too much sense):
Project model
Task model
Each task can be assigned to many projects
I want to be able to view a project's tasks at:
/projects/1/tasks
OR view all tasks in all projects at: /tasks
But having two controllers for this doesn't seem very DRY and doesn't seem like the Rails way. Is there a better way?
What you're looking for is essentially two routes to the same resources.
resources :tasks
resources :projects
match projects/:id/tasks => 'tasks#index'
In your TasksController#index, check for params[:id]
def index
if params[:id]
#tasks = Project.find(params[:id]).tasks
else
#tasks = Tasks.all
end
respond_with(#tasks)
end

Resources