ActiveAdmin how to Decorate associated links - ruby-on-rails

In ActiveAdmin, I know I can use decorators, like Draper, to feed display_name and name, but how do I use the decorator for simple association links (i.e. auto_link(resource))?
Given I have a Post & a Comment:
# Post.rb
class Post
has_many :comments
end
# Comment.rb
class Comment
belongs_to :post
end
# decorators/post_decorators.rb
class PostDecorator
def name
"Custom Post Name ##{object.id}"
end
end
# admin/post.rb
ActiveAdmin.register Post do
delegate_with PostDecorator
end
# admin/comments.rb
ActiveAdmin.register Comment do
index do
# ...
column :post
# ...
end
show do
default_main_content
end
end
When viewing the Comment ActiveAdmin area, the show's default_main_content and the index's column :post both link automatically to the Post object, but never use the decorator.
I will see: Post #4 instead of Custom Post Name #4 in those sections.
When I visit the Post admin area, it will use the decorated name perfectly fine.
How do I get automatic links to use Draper throughout the entire admin area?
I currently have a def name on the object itself, but that is a display property and want to move it to a Decorator.

If you're using Draper, you can use decorates_association to tell one decorator to decorate its associations. This requires that you have a CommentDecorator.
class CommentDecorator < Draper::Decorator
delegate_all
decorates_association :post
end
ActiveAdmin.register Comment do
decorate_with CommentDecorator
...
end

#nitsujri you can write a draper concern that you can include on all of your decorators that functionally handles all associations for you. Like you I was also tired of having to manage the associations myself - it meant keeping track of associations in another place.
Here is the simple concern that relies on using activerecord reflection to pull all of the names off of the existing object and throwing them to drapers decorates_associations method.
module AutoDecorateAssociations
extend ActiveSupport::Concern
included do
delegate :class, to: :object, prefix: true
decorates_associations *(object_class.reflect_on_all_associations.map(&:name) - [:versions])
end
end
and then just include AutoDecorateAssociations on the decorators you don't want to have to keep track of (unfortunately you can't toss it in your application_decorator)

Related

How do I create both a main and nested ActiveAdmin resource?

I have an Order resource, nested under User:
ActiveAdmin.register Order do
belongs_to :user
end
# Routes at:
# /admin/users/:user_id/orders/...
I would now also like to create an Order resource, for an overall view. Ideally I'd just do:
ActiveAdmin.register Order do
end
# Routes at:
# /admin/orders/...
But this doesn't work, because it's creating the same underlying class (I assume).
it appears based on this that I should be able to use as: 'all_orders', but in fact this still appears to affect the same class, and ends up with routes like /admin/users/:user_id/all_orders/...
So, how can I have both order resources set up and operating independently, both using orders in the URL?
I think this might be the best option, as detailed here:
ActiveAdmin.register Order do
belongs_to :user, optional: true
end
# Routes at:
# /admin/orders/...
# /admin/users/:user_id/orders/...
I would like to have the option of doing different things for the two, so an option where they can be separately defined would still be appreciated. If no better options are available I'll leave this answer here as it's reasonable.
Another solution, which is very hacky but does provide what I need is this:
# models/order.rb
class Order < ActiveRecord::Base
belongs_to :user
end
# models/order_alias.rb
class OrderAlias < Order
end
# admin/user/order.rb
ActiveAdmin.register Order do
belongs_to :user
end
# admin/order.rb
ActiveAdmin.register OrderAlias, as: 'AllOrder' do
menu label: 'Orders'
index title: 'Orders' do
# ...
end
end
This still has all_orders in the URL, but it's the closest to a solution I can find. Anything more elegant much appreciated.

Route using underscores instead of spaces

Ruby on Rails 4.0. I have a 'Specialty' model that 'admins' can modify using the standard resource routes. However there is also a separate consumer facing controller that is used to display those 'Specialties' in a pretty fashion. As such I have the following routes:
get "specialty/:name" => "site#specialty", as: :site_specialty
resources :specialties
The site#specialty controller action is as follows:
def specialty
#specialty = Specialty.find_by_name(params[:name])
end
This results in urls like the following percent escaped routes:
/specialty/project%20management
I would rather have something like this:
/specialty/project_management
How do I replace the spaces with underscores and still look up the correct model in the controller action? Any side notes on security also appreciated
Try using to_param:
Model:
class Specialty < ActiveRecord::Base
def to_param
name.parameterize
end
end
Controller:
def specialty
#specialty = Specialty.find(params[:id])
end
That should do it...
References:
http://guides.rubyonrails.org/active_support_core_extensions.html#to-param
https://gist.github.com/cdmwebs/1209732
http://railscasts.com/episodes/63-model-name-in-url
The answer by #manishie is good, but there is also a gem that handles this for you (and much more), called Friendly ID. It is based on the same to_param trick as previously mentioned, but also has options to handle other special characters and handle collisions.
class Specialty < ActiveRecord::Base
extend FriendlyId
friendly_id :name
end
Specialty.friendly.find(params[:name])

Overriding find in Mongoid has_many relationship

Using Mongoid 2.4.5 on Rails 3.2.1
I have a Model Book that has_many :pages.
class Book
include Mongoid::Document
has_many :pages
end
class Page
include Mongoid::Document
field :page_number
belongs_to :book
validates_uniqueness_of :page_number, scope: :book
end
I'm using nested resources so that I can get urls like /books/4f450e7a84b93e2b44000001/pages/4f4bba1384b93ea750000003/
What I would like to be able to do is use a url like /books/4f450e7a84b93e2b44000001/pages/3/ to get the third page in that book.
Now the crux of the question:
I want to find the page via a call like Book.find('4f450e7a84b93e2b44000001').pages.find('3') or like Book.find('4f450e7a84b93e2b44000001').pages.find('4f4bba1384b93ea750000003')
I know that I can override the find method in Page with something like
class << self
def find(*args)
where(:page_number => args.first).first || super(args)
end
end
But that doesn't seem to have any effect on the scoped query book.pages.find('3') as it seems the scoped search uses a different find method.
How do I specifically override the find method used by book.pages.find('3')?
Why just do a where criteria on your pages ?
Book.find('4f450e7a84b93e2b44000001').pages.where( :page_number => '3')
You can do a scope to in your Pages
class Page
scope :page_number, lambda{|num| where(:page_number => num) }
end
and use it like :
Book.find('4f450e7a84b93e2b44000001').pages.page_number('3')
Define a to_param method on your Page model that returns the page number. This way all Rails URL helpers use that when building URLs (automatically). Then you can just use something like
#book.pages.where(:page_number => params[:page_id]) # page_id is actually the result of page#to_param
Btw. I don't know how large your books are, but it might make more sense to embed your Pages in the Book from a document-oriented database point of view. The whole relationship business is not native to MongoDB.

How do I extract this so it can be reused in other parts of my app?

I have this method in my book model but now I realize I need this in a category model as well:
def proper_user?(logged_in_user)
return false unless logged_in_user.is_a? User
user == logged_in_user
end
I now have this method duplicated in the books model and the category model. Both category and books has belongs_to :user and both have the user_id:integer in the table as well. I simply want to extract this somewhere where so I can its DRY.
i tried to put the method in application_controller.rb but it says undefined method `proper_user?' for #
Thanks
Jeff
I think you'd want to be able to call this method like this:
book.proper_user?(current_user)
So it would work best to define it in each model rather then in User. This is best done by mixing in a module with the method:
module UserMethods
def proper_user?(logged_in_user)
# ... etc ...
end
end
and including it in each model:
class Book < AR::Base
include UserMethods
class Category < AR::Base
include UserMethods
The module can go in a source file in config/initializers, or you can put it elsewhere and change config.autoload_paths in config/environment.rb to point to the location.
Since it is related to the User model, why not put it there?

Best practice: How to split up associations-functions in controllers with equal-access models

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.

Resources