Let's say I have two different controller/view sets that are basically identical.
Business hours and Staff hours
The first contains a business_id, the latter a staff_id.
Otherwise they look the same. id, mon_start_time, mon_stop_time, tues_start_time, tues_stop_time, etc...
1)
Is there a way I would use the same controller for these since they are so similar? It doesn't seem to make THAT much sense, but I just keep thinking about how similar they are and how much duplicate code there is.
2)
Additionally, for each one I have a form in a partial. I'm trying to use the same one for both business hours and staff hours. The partial, in the most simplified state looks like:
-form_for(obj) do |f|
=f.error_messages
%p
=select obj.class, :mon_start_time, hours
to
=select obj.class, :mon_stop_time, hours
=link_to 'Back', obj_path
So there are 3 unique things I need to pass in. The 'obj', either business_hours or staff_hours. That's okay in the form_for, but how do I get the lowercase controller name for the first parameter of the selects? I need 'business_hour', and 'staff_hour', from 'obj'.
Then I need it to know the correct 'Back' link.
I know I can pass parameters into the partial, but I'm just curious if there's a slicker way of going about this.
Thanks!
Duplicate code has a carrying cost, a cost to maintain it. We sometimes don't know how high that cost is until we refactor the duplicate code and find outselves breathing a sigh of relief: Now I can change the business rule in just one place. Now I can stop typing things twice.
You can use two controllers but still refactor the duplicate code. One way is to put the common code in a module included by both controllers.
module CommonStuff
def stuff_that_is_the_same
end
end
controller FooController < ApplicationController
include CommonStuff
def stuff_that_is_different
# Stuff specific to Foo
stuff_that_is_the_same
# More stuff specific to Foo
end
end
controller BarController < ApplicationController
include CommonStuff
def stuff_that_is_different
# Stuff specific to Bar
stuff_that_is_the_same
# More stuff specific to Bar
end
end
As far as getting the name of controller is concerned you can get it in any controller action by calling the method
controller_name
and the controller state is available in #controller instance variable to your views, so if you want to access it in your views then you can do
#controller.controller_name
Now looking at your BusinessHours and StaffHours classes, I would say the best thing to do here to make them polymorphic. The first thing you will achieve here is to get rid of an almost identical table. So check out the rails core polymorphic docs
NOTE: But the has_many_polymorphs mentioned by #amurmann is not yet available in rails core, though you can use it as a plugin. Pratik wrote a blog post about it here
For removing duplicate code from the controller, you can either put that in a module (as #Wayne said) or create a base controller from which your Business and Staff hours controllers inherit all the common functionality. Now the solution totally depends what makes more sense in your application. Personally, I will create a base controller as it is more OO, keep my classes structured and code will not be hidden in some module. But some people may think otherwise.
Have you looked into Single Table Inheritance? I think it's the all around best solution for you here.
Essentially you define an hours table and model, that looks exactly like either staff hours or business hours and includes a string column named type. Then you create subclasses of Hours to define methods and validations specific to Business and Staff hours respectively. You also might want to redefine your association to something more generic. In the example, I've called it keeper, as on one who keeps those hours.
In a nutshell rails is handling all the polymorphic behaviour for you.
Models:
class Hour < ActiveRecord::Base
# common associations/methods etc go here
end
class BusinessHour < Hour
belongs_to :keeper, :class_name => "Business"
#business hour specific logic
end
class StaffHour < Hour
belongs_to :keeper, :class_name => "Staff"
#staff hour specific logic
end
class Business < ActiveRecord::Base
has_many :business_hours, :foreign_key => :keeper_id
...
end
class Staff < ActiveRecord::Base
has_many :staff_hours, :foreign_key => :keeper_id
...
end
Route:
map.resources :business_hours, :controller => "hours", :class => "BusinessHour"
map.resources :staff_hours, :controller => "hours", :class => "StaffHour"
Controller:
class HoursController < ApplicationController
before_filter :select_class
def select_class
#class = Kernel.const_get(params[:class])
end
def new
#hour = #class.new
...
end
def show
#hour = #class.find(params[:id])
...
end
...
end
Views will look almost exactly what you have now, but you're going to want some helpers to keep the output straight.
module HourHelper
def keeper_type
#class == "BusinessHours" ? "Business" : "Staff"
end
end
-form_for(obj) do |f|
=f.error_messages
%p =select keeper_type, :mon_start_time, hours
to
=select keeper_type, :mon_stop_time, hours
=link_to 'Back', obj_path
You might also want to consider creating a custom form builder to simplify the form creation.
It sounds to me like you might be able to combine both into one resource. Let's just call the resource "hours", for this discussiion.
The problem that "hour" might belong to either a business or staff could potentially be solved by making the relation polymorph.
You can for sure solve that relation problem with has_many_polymorphs:
class Hours < ActiveRecord::Base
has_many_polymorphs :participants, :from => [:staff, :business], :through => :hours_participants
end
This would require a new table and might not be the solution with the best performance. However, it should be a DRY solution.
Related
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
The Stage
Lets talk about the most common type of association we encounter.
I have a User which :has_many Post(s)
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
Problem Statement
I want to do some (very light and quick) processing on all the posts of a user. I am looking for the best way to structure my code to achieve it. Below are a couple of ways and why they work or don't work.
Method 1
Do it in the User class itself.
class User < ActiveRecord::Base
has_many :posts
def process_posts
posts.each do |post|
# code of whatever 'process' does to posts of this user
end
end
end
Post class remains the same:
class Post < ActiveRecord::Base
belongs_to :user
end
The method is called as:
User.find(1).process_posts
Why doesn't this look the best way to do it
The logic of doing something with the posts of the user should really belong to the Post class. In a real world scenario, a user might also have :has_many relations with a lot of other classes e.g. orders, comments, children etc.
If we start adding similar process_orders, process_comments, process_children (yikes) methods to the User class, it'll result in one giant file with lots of code much of which could (and should) be distributed to where it belongs i.e. the target associations.
Method 2
Proxy Associations and Scopes
Both of these constructs require addition of methods/code to the User class which again makes it bloated. I'd rather have all implementation shifted to the target classes.
Method 3
Class Method on target Class
Create class methods in the target class and call those methods on the User object.
class User < ActiveRecord::Base
has_many :comments
# all target specific code in target classes
end
class Post < ActiveRecord::Base
belongs_to :user
# Class method
def self.process
Post.all.each do |post| # see Note 2 below
# code of whatever 'process' does to posts of this user
end
end
end
The method is called as:
User.find(1).posts.process # See Note 1 below
Now, this looks and feels better than Method 1 and 2 because:
User model remains clutter free.
The process function is called process instead of process_posts. Now we can have a process for other classes as well and invoke them as: User.find(1).orders.process etc. instead of User.find(1).process_orders (Method 1).
Note 1:
Yes you can call a class method like this on a association. Read why here. TL;DR is that User.find(1).posts returns a CollectionProxy object which has access to class methods of the target (Post) class. It also conveniently passes a scope_attributes which stores the user_id of the user which called posts.process. This comes handy. See Note 2 below.
Note 2:
For people not sure whats going on when we do a Post.all.each in the class method, it returns all the posts of the user this method was called on as against all the posts in the database.
So when called as User.find(99).posts.process, Post.all executes:
SELECT "notes".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 99]]
which are all the posts for User ID: 99.
Per #Jesuspc's comment below, Post.all.each can be succinctly written as all.each. Its more idiomatic and doesn't make it look like we are querying all posts in the database.
The Answer I am looking for
Explains what is the best way to handle such associations. How do people do it normally? and if there are any obvious design flaws in Method 3.
There's a fourth option. Move this logic out of the model entirely:
class PostProcessor
def initialize(posts)
#posts = posts
end
def process
#posts.each do |post|
# ...
end
end
end
PostProcessor.new(User.find(1).posts).process
This is sometimes called the Service Object pattern. A very nice bonus of this approach is that it makes writing tests for this logic really simple. Here's a great blog post on this and other ways to refactor "fat" models: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
Personally, I think that Method 1 is the cleanest one. It will be very clean and understandable write something like this:
Class User < ActiveRecord::Base
has_many :posts
def process_posts
posts.each do |post|
post.process
end
end
end
And put all the logic of process method in Post model (with an instance variable):
Class Post < ActiveRecord::Base
belongs_to :user
def process
# Logic of your Post process
end
end
That way, the very logic of a Post process belong to Post class. Even if your User model will have many "process" functions, these will be very basic and small. That seems very clean to me, as a developer.
Method 3 has many technical implications that are pretty complex and unintuitive (yourself had to clarify your question).
NOTE: If you want better performance, maybe you should use eager loading to reduce ActiveRecord calls, but that is out of the scope of this question.
First of all excuse me for the opinionated answer.
ActiveRecord models are a controversial matter. Its essence is against the Single responsibility principle since they handle both database interaction via class methods and domain objects (which use to implement their own behaviour) via its instances. At the same time they also break the Liskov Substitution Principle because the models are not sub cases of ActiveRecord::Base and implement their own set of methods. And finally the ActiveRecord paradigm often leads to code that breaks the Law of Demeter, as in your proposal for the third method:
User.find(1).posts.process
Thus, there is a trend that in order to reduce coupling would recommend to use ActiveRecord objects only to interact with the database and therefore no behaviour should be added to them (in your case the process method). Under my point of view that is the lesser evil, even though it is still not a perfect solution.
So if I were to implement what you describe I would have a ProcessablePostsCollection object (where the name Processable can be customised to better describe what the processing is about, or even neglected completely so you would simple have a PostsCollection class) that would probably be a wrapper over a list of posts using SimpleDelegator and would have a method process.
class ProcessablePostsCollection < SimpleDelegator
def self.from_collection(collection)
new collection
end
def initialize(source)
super source
end
def process
# code of whatever 'process' does to posts
end
end
And the usage would be something like:
ProcessablePostsCollection.from_collection(User.find(1).posts).process
even though the from_collection and the call to process should happen in different clases.
Also, in case you have a big posts table it would probably be wise to process stuff in batches. For that your process method could call find_in_batches on your posts ActiveRecord::Relation.
But as always it depends on your needs. If you are simply building a prototype is perfectly fine to let your models grow fat, and if you are building an enormous application Rails itself is probably not going to be the best choice since discourages some OOP best practises with things such as ActiveRecord models.
You shouldn't be putting this in the User model - put it in Post (unless - of course - the scope of process involves the User model directly) :
#app/models/post.rb
class Post < ActiveRecord::Base
def process
return false if post.published?
# do something
end
end
Then you can use an ActiveRecord Association Extension to add the functionality to the User model:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :posts do
def process
proxy_association.target.each do |post|
post.process
end
end
end
end
This will allow you to call...
#user = User.find 1
#user.posts.process
I have a Boilerplate model which has two descending models:
BoilerplateOriginal
BoilerplateCopy
While BoilerplateOriginals is sort of a template that admins create, BoilerplateCopys are copies of the originals that are free to edit by everyone, and they also have some more associated objects (e.g. BoilerplateCopy belongs_to: BoilerplateOriginal, BoilerplateCopy belongs_to: Project or BoilerplateCopy has_many: Findings, all of which BoilerplateOriginal doesn't).
So in my opinion, it makes sense to maintain two different model classes that share the same basic functionalities.
Because they also look quite the same, I want to use the same views for them. But under the hood, they are treated a bit different, so I also have two different controllers inheriting from a base controller.
Everything seems to work fine, except that form_for boilerplate, as: resource_instance_name raises an exception undefined methodboilerplates_path', but only when called asnewaction, not when called asedit` action.
Here's what I have done so far to make it work (and everything else seems to work fine):
# config/routes.rb
resources :boilerplate_originals
# app/models/boilerplate.rb
class Boilerplate < ActiveRecord::Base
def to_partial_path
'boilerplates/boilerplate'
end
end
# app/models/boilerplate_original.rb
class BoilerplateOriginal < Boilerplate
end
# app/controllers/boilerplates_controller.rb
class BoilerplatesController < InheritedResources::Base
load_and_authorize_resource
private
def boilerplate_params
params.require(:boilerplate).permit(:title)
end
end
# app/controllers/boilerplate_originals_controller.rb
class BoilerplateOriginalsController < BoilerplatesController
defaults instance_name: 'boilerplate'
end
# app/views/boilerplates/_form.html.slim
= form_for boilerplate, as: resource_instance_name
# ...
As pointed out, new/create works flawlessly, but edit doesn't. And I'm using InheritedResources, by the way.
Rails is dong it correctly, you're slightly doing it wrong
the problem is:
resources :boilerplate_originals
that will just generate routes for especially boilerplate_originals.
when using form_helpers of rails rails will lookup for a route based on the models class which is in this case "boilerplate_copy". that means its looking for a edit_boilerplate_copy_path (which isnt generated by rails)
You said that BoilerplateCopy and BoilerplateOriginal are pretty much looking the same (i guess you just copy a model, there are gems out for doing that for you...)
If you go correct STI it "should" be
class Boilerplate; end
class BoilerplateCopy < Boilerplate; end
class BoilerplateOriginal < Boilerplate; end
for that you need only a route for Boilerplate
resources :boilerplate
and of course a boilerplate_controller
everything is handled as a boilerplate and the form_helper will look up for a new_boilerplate_path, which exists, no matter if its a copy or a original.
I found the problem.
# app/controllers/boilerplate_originals_controller.rb
class BoilerplateOriginalsController < BoilerplatesController
defaults instance_name: 'boilerplate' # This is wrong!
defaults instance_name: 'boilerplate', resource_class: BoilerplateOriginal # This is right!
end
Now a BoilerplateOriginal object is passed to the new action, and not a Boilerplate object.
I have a workflow app written in Rails and there are a number of prompts which the user will go through. When these prompts/manual actions are "proceeded" a time stamp is stored against the a workflow model.
Code looks something like so
class Workflow < ActiveRecord::Base
end
class OpenBook < ActiveRecord::Base
belongs_to :workflow, :polymorphic => true
end
class ReadFirstPage <
belongs_to :workflow, :polymorphic => true
end
At the moment each workflow item ( OpenBook ) and spits out a bit of text (set in the model) and a forward / backward button on the view. The controllers for these workitems also do the similar things (new, done, edit, proceed, rewind).
Ideally I would like to just use the same controller and views and just change the model. Is theer something I can do with the routes eg?
resources :open_books, :controller => "workflow_item"
Im not sure how I would go about getting the correct assignments in the controller.
Or am I just doing this completely wrong and I should be using helpers?
You might have an easier time using a series of controllers for each specific object type that inherit from a common parent. That way most of the duplication can be in the base class where only the differences are expressed in the children.
To ensure your templates are rendered correctly, you may have to specify the full path to them or it will look for customized versions:
def index
# ...
render(:template => 'parent/index')
end
Usually this is redundant, but in your case you may need it or it will default to showing the non-existent child-template first.
I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.