I know this is a really simple question but I guess my brain and google-fu isn't working so well today.
Let's say I have an Event, with Registrants, and they can pay for the event using one or more payments.
I'm trying to create a payment linked to a registrant (who is linked to an event).
So my payment should have both registrant_id and event_id.
My URL looks something like this: (nested routes)
http://mysite.com/events/1/registrants/1/payments/new
My controller looks something like:
def create
#event = Event.find(params[:event_id])
#registrant = Registrant.find(:first, conditions: {id: params[:registrant_id], event_id: params[:event_id]} )
#payment = Payment.new params[:payment]
end
I know there is a much better way to do it, but I'm having trouble with the wording to properly google it :)
What syntax should I be using to make the .new automatically aware of the event_id and registrant_id?
Based on the discussion in the comments, there are several ways that the question can be addressed: the direct way and the Rails way.
The direct approach to creating objects that are related is to create the object using new_object = ClassName.new as suggested in the question. Then take the id of the created object and set that on an existing object (directly with existing_object.id = new_object.id or through some other method if additional logic is required). Or set the id on a new object by defining a custom initializer, such as:
class Payment
def initializer id_of_registrant
#registrant_id = id_of_registrant
end
...
end
The advantage of this approach is that it allows you to assign registrant IDs that may come from a range of objects with different classes, without having to deal with unnecessary or perhaps incorrect (for your solution) inheritance and polymorphism.
The Rails way, if you always have a direct relationship (1 to 1) between a Registrant and a 'mandatory' Payment is to use a has_many or belongs_to association, as described in the Rails guide: http://guides.rubyonrails.org/association_basics.html
For the example classes from the question:
class Registrant < ActiveRecord::Base
has_one :payment
end
class Payment < ActiveRecord::Base
belongs_to :registrant
end
You will want to use the appropriate migration to create the database tables and foreign keys that go with this. For example:
class CreateRegistrants < ActiveRecord::Migration
def change
create_table :registrants do |t|
t.string :name
t.timestamps
end
create_table :payments do |t|
t.integer :registrant_id
t.string :account_number
t.timestamps
end
end
end
Of course, if you registrants only optionally make a payment, or make multiple payments, then you will need to look at using the has_many association.
With the has and belongs associations, you can then do nice things like:
#payment.registrant = #registrant
if you have instantiated the objects by hand, or
#payment.new(payment_amount)
#registrant = #payment.build_registrant(:registrant_number => 123,
:registrant_name => "John Doe")
if you would like the associations populated automatically.
The Rails Guide has plenty of examples, though in my experience only trying the most appropriate one for your actual use case will show if there are restrictions that could not be anticipated. The Rails approach will make future queries and object building much easier, but if you have a very loose relationship model for your objects you may find it becomes restrictive or unnatural and the equivalent associations are better coded by hand with your additional business rules.
It's not great practice to set id attributes directly, as the id might not refer to an actual database row. The normal thing to do here would be to use CanCan (https://github.com/ryanb/cancan), which seems like it would solve all your problems.
EDIT:
If you're not using authentication of any kind then I'd either put the load methods in before_filters to keep things clean:
before_filter :load_event
def load_event
#event = Event.find params[:event_id]
end
or define some funky generic loader (unnecessarily meta and complex and not recommended):
_load_resource :event
def self._load_resource resource_type
before_filter do
resource = resource_type.constantize.find params[:"#{ resource_type }_id]
instance_variable_set :"##{ resource_type }", resource
end
end
Related
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.
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
So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly
I am building a Ruby on Rails 4.1 app that has the following models that should make sense when you see the model names:
Domains, Teams, Users, Meetings etc
Now, a Team belongs to a domain and a User to a team, also meetings belong to a user. A domain is simply an organisation or company that is using the software.
After the creation of an admin user that user has to initially create a Domain, then create the first team, then other users can sign up.
As you can probably anticipate, in the process of creating the initial Domain and Team (which is done in the new/create of the Team and Domain controllers) I am having to access and change references in all three. So, for example, creating the first Domain will have to associate the admin user, creating a Team will involve linking it to the user, then also the parent domain.
So, it's all getting a bit mixed up, with the controller needing to access several models.
My question is, where does this logic belong? In the spirit of thin controller you would normally farm it out to the model, but it involves more than one model. Is this where the new Rails 4 concerns become useful or should I just put it in the controller?
I am relatively new to rails, so please keep that in mind if you are kind enough to reply - thanks!
Nested resources
class Domain
has_many :teams
end
class Team
has_many :users
belongs_to :domain
end
class User
has_many :meetings
belongs_to :team
end
class Meeting
belongs_to :user
end
Then in your controllers:
class TeamsController < ApplicationController
def new
#team = Team.new
end
def create
#domain = Domain.find(params[:domain_id])
#team = #domain.teams.build(params[:team])
#team.save
respond_with #team
end
end
That's what we call a "nested" controller, in the routes.rb file:
resources :domain do
resources :team
end
The URL will look like that:
/domains/:domain_id/teams
/domains/:domain_id/teams/:team_id
Same logic apply for other models. This should give you a starting point to build your application.
The following line
#domain.teams.build(params[:team])
automatically link a domain to a team setting the reference (id) for you.
However you should not do deep nesting according to rails guide so that's where the builder design pattern can come in handy.
Builder design pattern
However, if things start to get to messy, I would suggest using a dedicated ruby class to "build" your objects and their relationship. We usually call those classes a "Builder":
class TeamBuilder
attr_reader :domain, :params
def initialize(domain, params = {})
#domain = domain
#params = params
end
def build
domain.teams.build(params)
end
end
Here it's again doing very simple task. For user and meetings for example:
class UserBuilder
attr_reader :team, :params
def initialize(team, params = {})
#team = team
#params = params
end
def build
team.users.build(params).tap do |user|
user.foo = 'foo'
user.meetings.build(...)
user.meetings << MeetingBuilder.new(user, { ... })
end
end
end
class MeetingBuilder
# ...
end
Here we use the MeetingBuilder within the UserBuilder to build a meeting.
Usage:
user = UserBuilder.new(team, { ... }).build
user.save
Ideally, a model shouldn't care about, or even know about, other classes. So between the model and the controller, this kind of logic definitely belongs in the controller.
I would probably go with a third class though, taking care of that messy stuff, like a DomainFactory for example. Which is just a plain old ruby object (poro), no active record or anything, with the sole purpose of creating domains.
A tip is to read up on loose coupling and single responsibility.
I've done some searching here and I've not been able to find anything that quite answers what I'm looking for. If I failed in my search I apologize.
Moving on, I am new to Rails and I'm working on an application to test the waters if you will. I'm using Devise for authentication and it's proving quite useful. That said, I've run into a big of a road block with where a certain check for data would go, and how I would go about it.
I have three tables: users, games, and users_games (I read that this was an acceptable naming convention for relational tables, correct me if I'm wrong). On a Games page I would like to display a certain message if the currently logged in User has this Game added to their account (in users_games). I'm unsure of where to perform this check, or if it even matters at all.
As for the actual checking, my initial idea would be something along the lines of:
games_controller.rb
class GamesController < ApplicationController
def index
#games = Game.all
end
def show
#game = Game.find(params[:id])
#user_owns = UsersGames.where(:game_id => #game.id, :user_id => current_user.id)
end
end
Then on the view checking if #user_owns has a value or not.
Thanks in advance for any insight or wisdom you can offer.
What about this way, may be you don't need users_games
if game has_many users and user belongs_to game
def show
#game = Game.find_by_user_id(current_user.id)
end
Then on the view checking if #game has a value or not.
If your Users<->Games relationship is a simple HABTM with no additional attributes on the join table, i.e.
class User < AR::Base
has_and_belongs_to_many :games
class Game < AR::Base
has_and_belongs_to_many :users
you don't need to have a separate model for the join table, provided that you follow the Rails naming convention that requires you to follow the lexicographical order when naming your join table, i.e. in your case it would be games_users, not the other way around like you have it now.
Going back to your original question, I think it can be as simple as this:
def show
#game = Game.find(params[:id])
#game_owned = current_user.games.include? #game
end
You can also make this a method on your User model:
class User < AR::Base
...
def owns_game?(game)
self.games.include?(game)
end
end
and then call current_user.owns_game?(#game) in your controllers/views.