Rails: MVC How to use custom actions - ruby-on-rails

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.

Related

Rails API: One Request, Multiple Controller Actions

I have multiple models that in practice are created and deleted together.
Basically I have an Article model and an Authorship model. Authorships link the many to many relation between Users and Articles. When an Article is created, the corresponding Authorships are also created. Right now, this is being achieved by POSTing multiple times.
However, say only part of my request works. For instance, I'm on bad wifi and only the create article request makes it through. Then my data is in a malformed half created, half not state.
To solve this, I want to send all the data at once, then have Rails split up the data into the corresponding controllers. I've thought of a couple ways to do this. The first way is having controllers handle each request in turn, sort of chaining them together. This would require the controllers to call the next one in the chain. However, this seems sorta rigid because if I decide to compose the controllers in a different way, I'll have to actually modify the controller code itself.
The second way splits up the data first, then calls the controller actions with each bit of data. This way seems more clean to me, but it requires some logic either in the routing or in a layer independent of the controllers. I'm not really clear where this logic should go (another controller? Router? Middleware?)
Has anybody had experience with either method? Is there an even better way?
Thanks,
Nicholas
Typically you want to do stuff like this -- creating associated records on object creation -- all in the same transaction. I would definitely not consider breaking up the creation of an Authorship and Article if creating an Authorship is automatic on Article creation. You want a single request that takes in all needed parameters to create an Article and its associated Authorship, then you create both in the same transaction. One way would be to do something like this in the controller:
class Authorship
belongs_to :user
belongs_to :article
end
class Article
has_many :authorships
has_many :users, through: :authorships
end
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
#article.authorships.build(article: #article, user_id: params[:user_id])
if #article.save
then do stuff...
end
end
end
This way when you hit #article.save, the processing of both the Article and the Authorship are part of the same transaction. So if something fails anywhere, then the whole thing fails, and you don't end up with stray/disparate/inconsistent data.
If you want to assign multiple authorships on the endpoint (i.e. you take in multiple user id params) then the last bit could become something like:
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
params[:user_ids].each do |id|
#article.authorships.build(article: #article, user_id: id)
end
if #article.save
then do stuff...
end
end
end
You can also offload this kind of associated object creation into the model via a virtual attribute and a before_save or before_create callback, which would also be transactional. But the above idiom seems more typical.
I would handle this in the model with one request. If you have a has_many relationship between Article and Author, you may be able to use accept_nested_attributes_for on your Article model. Then you can pass Authorship attributes along with your Article attributes in one request.
I have not seen your code, but you can do something like this:
model/article.rb
class Article < ActiveRecord::Base
has_many :authors, through: :authorship # you may also need a class_name: param
accepts_nested_attributes_for: :authors
end
You can then pass Author attributes to the Article model and Rails will create/update the Authors as required.
Here is a good blog post on accepts_nested_attributes_for. You can read about it in the official Rails documentation.
I would recommend taking advantage of nested attributes and the association methods Rails gives you to handle of this with one web request inside one controller action.

What routes are necessary when the Model and Controller names do not match?

I have a Model called Category and another called Articles. Categories are "sections" that have many Articles, for instance News and Events. Both Categories use the kind of Articles, except they're shown under a different section of my website.
Right now I'm creating the News controller (NewsController), and I'd like to visit /news/new to add News. Likewise, the same would apply to EventsController and /events/new.
What do I have to use on my routes to do this?
My first attempt was to use:
resources :categories do
resources :articles, path: '/news'
end
But this forces me to use /categories/1/news/new, which is kinda ugly.
If News will always be category_id 1 and Events will always be 2, how would I specify this on my routes, so I can easily access them with the URLs I mentioned?
Explained Differently
I have an Articles model. I'd like to have a controller called NewsController to handle Articles, so that /news/new (and the rest of the paths) would work with Article. I'd also like to have a controller called EventsController that would also handle Articles, so that /events would also work with Article. The difference between them is that they have different category_id.
Is this possible to do via routes?
Update
Made some progress.
resources :categories do
resources :articles
end
get 'news/new' => 'articles#new', defaults: {category_id: 1}
get 'events/new' => 'articles#new', defaults: {category_id: 2}
This fixes what I wanted to do with /news/new and /events/new, but I'd be missing the rest of the routes (edit, show, update, etc). Also, this makes me use the Articles controller, which currently does not exist and would also make the News controller obsolete/useless.
My logic may be wrong, it's kinda evident with what I just made, but perhaps with this update I can better illustrate what I'm trying to do.
Update 2
I'm currently testing the following:
resources :articles, path: '/news', controller: 'news'
resources :articles, path: '/events', controller: 'events'
So far it makes sense, it makes the routes I wanted, it uses both controllers with their own configurations, and it hasn't spat any errors when I visit both /news and /events (yet).
It's also possible to do:
resources :articles, path: '/news', defaults: {category_id: 1}
resources :articles, path: '/events', defaults: {category_id: 2}
But this would depend on an Article controller, which could handle both types of Categories. Either solution works (theoretically), though I'd incline more on the first since the individual controllers would allow more specific configuration to both cases. The second, though, is more adequate when there're not that many difference between the Articles being created. The defaults property isn't explicitly necessary either, I just put it there for convenience.
Your question is asking something that I question as not making sense and maybe your design is flawed.
Why would you have news resources related to category resources if they are not related?
Is categories just a name space?
If news records really are always going to be related to the same first category as your question implies then you can not use ID's as you have no control over what the id will be for the first category and the first category could have an ID of anything in which case you could just use the top level news resources and do a find first category in your model in a before create then you don't have to worry about an ugly url.
If news records really are related to categories then the you must supply the relevant category id and nest your routes but you could pretty up the url using the following from
https://gist.github.com/jcasimir/1209730
Which states the following
Friendly URLs
By default, Rails applications build URLs based on the primary key --
the id column from the database. Imagine we have a Person model and
associated controller. We have a person record for Bob Martin that has
id number 6. The URL for his show page would be:
/people/6
But, for aesthetic or SEO purposes, we want Bob's name in the URL. The
last segment, the 6 here, is called the "slug". Let's look at a few
ways to implement better slugs. Simple Approach
The simplest approach is to override the to_param method in the Person
model. Whenever we call a route helper like this:
person_path(#person)
Rails will call to_param to convert the object to a slug for the URL.
If your model does not define this method then it will use the
implementation in ActiveRecord::Base which just returns the id.
For this method to succeed, it's critical that all links use the
ActiveRecord object rather than calling id. Don't ever do this:
person_path(#person.id) # Bad!
Instead, always pass the object:
person_path(#person)
Slug Generation
Instead, in the model, we can override to_param to include a
parameterized version of the person's name:
class Person < ActiveRecord::Base def to_param
[id, name.parameterize].join("-") end end
For our user Bob Martin with id number 6, this will generate a slug
6-bob_martin. The full URL would be:
/people/6-bob-martin
The parameterize method from ActiveSupport will deal with converting
any characters that aren't valid for a URL. Object Lookup
What do we need to change about our finders? Nothing! When we call
Person.find(x), the parameter x is converted to an integer to perform
the SQL lookup. Check out how to_i deals with strings which have a mix
of letters and numbers:
"1".to_i
=> 1
"1-with-words".to_i
=> 1
"1-2345".to_i
=> 1
"6-bob-martin".to_i
=> 6
The to_i method will stop interpreting the string as soon as it hits a
non-digit. Since our implementation of to_param always has the id at
the front followed by a hyphen, it will always do lookups based on
just the id and discard the rest of the slug. Benefits / Limitations
We've added content to the slug which will improve SEO and make our
URLs more readable.
One limitation is that the users cannot manipulate the URL in any
meaningful way. Knowing the url 6-bob-martin doesn't allow you to
guess the url 7-russ-olsen, you still need to know the ID.
And the numeric ID is still in the URL. If this is something you want
to obfuscate, then the simple scheme doesn't help. Using a Non-ID
Field
Sometimes you want to get away from the ID all together and use
another attribute in the database for lookup. Imagine we have a Tag
object that has a name column. The name would be something like ruby
or rails. Link Generation
Creating links can again override to_param:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end end
Now when we call tag_path(#tag) we'd get a URL like /tags/ruby. Object
Lookup
The lookup is harder, though. When a request comes in to /tags/ruby
the ruby will be stored in params[:id]. A typical controller will call
Tag.find(params[:id]), essentially Tag.find("ruby"), and it will fail.
Option 1: Query Name from Controller
Instead, we can modify the controller to
Tag.find_by_name(params[:id]). It will work, but it's bad
object-oriented design. We're breaking the encapsulation of the Tag
class.
The DRY Principle says that a piece of knowledge should have a single
representation in a system. In this implementation of tags, the idea
of "A tag can be found by its name" has now been represented in the
to_param of the model and the controller lookup. That's a maintenance
headache. Option 2: Custom Finder
In our model we could define a custom finder:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end
def self.find_by_param(input)
find_by_name(input) end end
Then in the controller call Tag.find_by_param(params[:id]). This layer
of abstraction means that only the model knows exactly how a Tag is
converted to and from a parameter. The encapsulation is restored.
But we have to remember to use Tag.find_by_param instead of Tag.find
everywhere. Especially if you're retrofitting the friendly ID onto an
existing system, this can be a significant effort. Option 3:
Overriding Find
Instead of implementing the custom finder, we could override the find
method:
class Tag < ActiveRecord::Base #... def self.find(input)
find_by_name(input) end end
It will work when you pass in a name slug, but will break when a
numeric ID is passed in. How could we handle both?
The first temptation is to do some type switching:
class Tag < ActiveRecord::Base #... def self.find(input)
if input.is_a?(Integer)
super
else
find_by_name(input)
end end end
That'll work, but checking type is very against the Ruby ethos.
Writing is_a? should always make you ask "Is there a better way?"
Yes, based on these facts:
Databases give the id of 1 to the first record
Ruby converts strings starting with a letter to 0
class Tag < ActiveRecord::Base #... def self.find(input)
if input.to_i != 0
super
else
find_by_name(input)
end end end
Or, condensed down with a ternary:
class Tag < ActiveRecord::Base #... def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Our goal is achieved, but we've introduced a possible bug: if a name
starts with a digit it will look like an ID. If it's acceptable to our
business domain, we can add a validation that names cannot start with
a digit:
class Tag < ActiveRecord::Base #... validates_format_of :name,
:without => /^\d/ def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Now everything should work great! Using the FriendlyID Gem
Does implementing two additional methods seem like a pain? Or, more
seriously, are you going to implement this kind of functionality in
multiple models of your application? Then it might be worth checking
out the FriendlyID gem: https://github.com/norman/friendly_id Setup
The gem is just about to hit a 4.0 version. As of this writing, you
want to use the beta. In your Gemfile:
gem "friendly_id", "~> 4.0.0.beta8"
Then run bundle from the command line. Simple Usage
The minimum configuration in your model is:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id :name
end
This will allow you to use the name column or the id for lookups using
find, just like we did before. Dedicated Slug
But the library does a great job of maintaining a dedicated slug
column for you. If we were dealing with articles, for instance, we
don't want to generate the slug over and over. More importantly, we'll
want to store the slug in the database to be queried directly.
The library defaults to a String column named slug. If you have that
column, you can use the :slugged option to automatically generate and
store the slug:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id
:name, :use => :slugged end
Usage
You can see it in action here:
t = Tag.create(:name => "Ruby on Rails")
=> #
Tag.find 16
=> #
Tag.find "ruby-on-rails"
=> #
t.to_param
=> "ruby-on-rails"
We can use .find with an ID or the slug transparently. When the object
is converted to a parameter for links, we'll get the slug with no ID
number. We get good encapsulation, easy usage, improved SEO and easy
to read URLs.
If you are sure there will be only 2 categories, why not simply add a boolean to the articles?
Like: article.event = true if events category, false if news
Then you can add a scopes to Article class for both categories
class Article
scope :events, -> { where(event: true) }
scope :news, -> { where(event: false) }
end
Create controllers, for example:
class EventsController < ApplicationController
def index
#articles = Article.events
end
def create
#article.new(params)
#article.event = true
#article.save
end
...
end
and routes: resources :events
You should try to use dynamic segments: http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
Add some slug attribute to Category, it should be unique and add index to it.
# routes
resources :articles, except: [:index, :new]
get '*category_slug/new', to: 'articles#new'
get '*category_slug', to: 'articles#index'
# controller
class ArticlesController < ApplicationController
def index
#category = Category.find_by slug: params[:category_slug]
#articles = #category.articles
end
def new
#category = Category.find_by slug: params[:category_slug]
#article = #category.articles.build
end
...
end
Remember to put a category in a hidden field in the form_for #article

Rails how to use 2 of the same slugs

I am using slugs in my project to give my params an other name but I have two params called: "how-does-it-work".
(.../investor/how-does-it-work)
(.../customer/how-does-it-work)
I would like to use the slugs as how they are currently set.
Is there a way to do that?
Create two distinct routes/controllers, and simply query the corresponding ActiveRecord model in the show action. Assuming there is a slug field on your models:
Rails.application.routes.draw do
resources :customers
resources :investors
end
class CustomersController < ApplicationController
def show
#customer = Customer.find_by(slug: params[:id])
end
end
class InvestorsController < ApplicationController
def show
#investor= Investor.find_by(slug: params[:id])
end
end
This is probably the most conventional way to solve this problem in Rails. If you are using the friendly_id gem, the same approach more or less applies, except for maybe the query itself.
Hope this helps.
So, is /investor/ and /customer/ both parts of the slug?
If that's the case, you can split the string, and do a search based on the "how-does-it-work" in the grouping of "investor" or "customer".
If investor and customer are both parts of the routes, you shouldn't have a difficult time there, because they're pointing to two different controller methods. You should be able to write a search based on each of those methods that correspond to the data. If the data is the same, all your doing is pointing the controller to the correct model data with the correct params.
If you're using friendlyId, it usually has built in candidate matching. Also, if you're meaning to match multiple pages to the same slug (which I've done in the past), you can display a results page if you'd like too, by rendering based on the quantity of results.

How to reduce number of calls to a helper method

In my view, I need a User object to display a few different properties. There is an instance variable #comments that's being sent from the controller. I loop through the comments and get the User information through a helper method in order to reduce db calls.
Here is the helper method:
def user(id)
if #user.blank? == false && id == #user.id
return #user
else
return #user = User.find(id)
end
end
And in the view, I display the details as follows:
<h4> <%=user(comment.user_id).name%> </h4>
<p><%=user(comment.user_id).bio%></p>
<p><%=user(comment.user_id).long_bio%></p>
<p><%=user(comment.user_id).email%></p>
<hr>
<p><%=user(comment.admin_id).bio%></p>
<p><%=user(comment.admin_id).long_bio%></p>
<p><%=user(comment.admin_id).email%></p>
I was told that assigning a variable in the view is bad practice and hence I am calling the helper method multiple times instead of assigning the returned User object.
Is there a better way to do this?
I think you are overcomplicating things here.
Let's say you have a user model
class User < ActiveRecord::Base
has_many :comments
end
an admin model
class Admin < User
end
a comment model
class Comment < ActiveRecord::Base
belongs_to :user
end
Now you only need a type column in your users table and you can do things like this:
Admin.all (All users with type "Admin")
User.all (Really all users including type "Admin" and all other types)
and for every comment you can just use
comment.user.bio
and it doesn't matter if it's an admin or not.
See http://www.therailworld.com/posts/18-Single-Table-Inheritance-with-Rails for example
Additional info: To reduce db calls in general(N+1 queries) watch http://railscasts.com/episodes/372-bullet
It's perfectly fine to pass models to your view and build the data on the view off of the data contained in the model. Keep in mind that I'm not entirely certain how you want your page to work, but one option you may have is to use a partial view and pass it the user object. This allows you to still only have the one model in your partial view without setting additional variables.
Also, without knowing what kind of database you're using or if your models have any associations, and assuming that you're doing some input validation, you may not need this helper method and may be able to lean on your ORM to get the user object.
For Example:
<%= comment.user.age %>
This isn't any more efficient than what you've currently got, but it certainly makes the code look cleaner.
Another alternative: set a user variable in the view. You're not performing logic in your view at this point, you're simply storing some data to the heap for later use.

Rails forms - Should I build `accepts_nested_attributes_for` associations in the Controller, Model, or View?

The Question
I have a parent that accepts_nested_attributes_for a child. So, when I have a form for the parent, I need to build the child so I can display form fields for it as well. What I want to know is: where should I build the child? In the Model, View, or Controller?
Why I Am Asking This
You may be shaking your head and thinking I'm a madman for asking a question like this, but here's the line of thinking that got me here.
I have a Customer model that accepts_nested_attributes_for a billing_address, like so:
class Customer
belongs_to :billing_address, class_name: 'Address'
accepts_nested_attributes_for :billing_address
end
When I present a form for a new Customer to the user, I want to make sure there is a blank billing_address, so that the user actually sees fields for the billing_address. So I have something like this in my controller:
def new
#customer = Customer.new
#customer.build_billing_address
end
However, if the user doesn't fill out any of the billing_address fields, but tries to submit an invalid form, they will be presented with a form that no longer has fields for the billing_address, unless I put something like this in the create action of my controller:
def create
#customer = Customer.new(params[:customer])
#customer.build_billing_address if #customer.billing_address.nil?
end
There is another issue, which is that if a user tries to edit a Customer, but that Customer doesn't have an associated billing_address already, they won't see fields for the billing_address. So I have to add somethign like this to the controller:
def edit
#customer = Customer.find(params[:id])
#customer.build_billing_address if #customer.billing_address.nil?
end
And something similar needs to happen in the controller's update method.
Anyway, this is highly repetitive, so I thought about doing something in the model. My initial thinking was to add a callback to the model's after_initialize event, like so:
class CustomerModel
after_initialize :build_billing_address, if: 'billing_address.nil?'
end
But my spidey sense started tingling. Who's to say I won't instantiate a Customer in some other part of my code in the future and have this wreak havoc in some unexpected ways.
So my current thinking is that the best place to do this is in the form view itself, since what I'm trying to accomplish is to have a blank billing_address for the form and the form itself is the only place in the code where I know for sure that I'm about to show a form for the billing_address.
But, you know, I'm just some guy on the Internet. Where should I build_billing_address?
Though this advice by Xavier Shay is from 2011, he suggests putting it in the view, "since this is a view problem (do we display fields or not?)":
app/helpers/form_helper.rb:
module FormHelper
def setup_user(user)
user.address ||= Address.new
user
end
end
app/views/users/_form.html.erb:
<%= form_for setup_user(#user) do |f| %>
Note that I had to change the helper method to the following:
def setup_user(user)
user.addresses.build if user.addresses.empty?
user
end
The controller remains completely unchanged.
If you know your model should always have a billing address, you can override the getter for this attribute in your model class as described in the docs:
def billing_address
super || build_billing_address
end
Optionally pass in any attributes to build_billing_address as required by your particular needs.
You would use build if you want to build up something and save it later. I would say, use it in nested routes.
def create
#address = #customer.billing_addresses.build(params[:billing_address])
if #address.save
redirect_to #customer.billing_addresses
else
render "create"
end
end
Something like that. I also use the build when I'm in the console.
You have to remember the principles of MVC, which is to create DRY(don't repeat yourself) code, which is efficiently distributed between the various moving parts of the app
accepts_nested_attributes_for Is Great For Keeping Things DRY
accepts_nested_attributes_for is a model function which allows you to pass data through an association to another model. The reason why it exists is to give you the ability to populate another model's data based on a single form, and is excellent for extending functionality without too much extra code
The problem you're citing is that if you want to use the code in other areas of the app, you'll end up having all sorts of problems
My rebuttal to that is in order to create as efficient an application as possible, you want to write as little code as possible - letting Rails handle everything. The accepts_nested_attributes_for function does allow you to do this, but obviously has a cost, in that you have to accommodate it every time you want to use it
My recommendation is to use what you feel is the most efficient code you can, but also keep to conventions; as this will ensure speed & efficiency
You should handle all these scenarios in controller, since it is not a responsibility of model.
Just in terms of keeping things DRY, you can write a method,
def build_customer(customer)
customer.build_billing_address if customer.billing_address.nil?
#add more code if needed
end
And inside controller you can call this method wherever it is needed. e.g.
def create
#customer = Customer.new(params[:customer])
if #customer.save
redirect_to #customer.billing_addresses
else
build_customer(#customer)
render "new"
end
end

Resources