Skip to content

Decorate associated objects #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,48 +65,6 @@ end
<% end %>
```

## Decorating associated objects ##

ActiveDecorator *does not* automatically decorate associated objects. We recommend that you pass associated objects to `render` when decorated associated objects are needed.

```ruby
# app/models/blog_post.rb
class BlogPost < ActiveRecord::Base
# published_at:datetime
end

# app/models/user.rb
class User < ActiveRecord::Base
has_many :blog_posts
end

# app/decorators/blog_post_decorator.rb
module BlogPostDecorator
def published_date
published_at.strftime("%Y.%m.%d")
end
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
end
```

```erb
# app/views/users/index.html.erb
<% @users.each do |user| %>
<%= render partial: "blog_post", locals: { blog_posts: user.blog_posts } %><br>
<% end %>

# app/views/users/_blog_post.html.erb
<% blog_posts.each do |blog_post| %>
<%= blog_post.published_date %>
<% end %>
```

## Testing

You can test a decorator using your favorite test framework by decorating the model instance with
Expand Down
2 changes: 2 additions & 0 deletions lib/active_decorator/decorated.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module ActiveDecorator::Decorated
end
9 changes: 9 additions & 0 deletions lib/active_decorator/decorator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'singleton'
require 'active_decorator/helpers'
require 'active_decorator/decorated'

module ActiveDecorator
class Decorator
Expand Down Expand Up @@ -28,13 +29,21 @@ def decorate(obj)
obj.extend ActiveDecorator::RelationDecoratorLegacy unless obj.is_a? ActiveDecorator::RelationDecoratorLegacy
end
else
if defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Base) && !obj.is_a?(ActiveDecorator::Decorated)
obj.extend ActiveDecorator::Decorated
end

d = decorator_for obj.class
return obj unless d
obj.extend d unless obj.is_a? d
obj
end
end

def decorate_association(owner, target)
owner.is_a?(ActiveDecorator::Decorated) ? decorate(target) : target
end

private
def decorator_for(model_class)
return @@decorators[model_class] if @@decorators.key? model_class
Expand Down
27 changes: 27 additions & 0 deletions lib/active_decorator/monkey/active_record/associations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module ActiveDecorator
module Monkey
module ActiveRecord
module Associations
module Association
def target
ActiveDecorator::Decorator.instance.decorate_association(owner, super)
end
end

module CollectionAssociation
# @see https://github.com/rails/rails/commit/03855e790de2224519f55382e3c32118be31eeff
if Rails.version.to_f < 4.1
def first_or_last(*args)
ActiveDecorator::Decorator.instance.decorate_association(owner, super)
end
else
def first_nth_or_last(*args)
ActiveDecorator::Decorator.instance.decorate_association(owner, super)
end
end
end
end
end
end
end
6 changes: 6 additions & 0 deletions lib/active_decorator/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class Railtie < ::Rails::Railtie
ActionMailer::Base.send :include, ActiveDecorator::ViewContext::Filter
end
end

ActiveSupport.on_load :active_record do
require 'active_decorator/monkey/active_record/associations'
ActiveRecord::Associations::Association.send :prepend, ActiveDecorator::Monkey::ActiveRecord::Associations::Association
ActiveRecord::Associations::CollectionAssociation.send :prepend, ActiveDecorator::Monkey::ActiveRecord::Associations::CollectionAssociation
end
end
end
end
7 changes: 7 additions & 0 deletions test/fake_app/app/views/authors/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
<%= @author.name %>
<%= @author.capitalized_name %>
<%= @author.books.first.upcased_title %>
<%= @author.books[-1].upcased_title %>
<% if p = @author.publishers.last %><%= p.upcased_name %><% end %>
<% if p = @author.profile %><%= p.address %><% end %>
<% if h = @author.profile_history %><%= h.update_date %><% end %>
<% if m = @author.magazines.first %><%= m.upcased_title %><% end %>
<% if c = @author.company %><%= c.reverse_name %><% end %>
1 change: 1 addition & 0 deletions test/fake_app/app/views/movies/show.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<%= @movie.name %>
<% if a = @movie.author %><%= a.reverse_name %><% end %>
63 changes: 60 additions & 3 deletions test/fake_app/fake_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,39 @@ class Application < Rails::Application
# models
class Author < ActiveRecord::Base
has_many :books
has_many :publishers, through: :books
has_many :movies
has_one :profile
has_one :profile_history, through: :profile
has_and_belongs_to_many :magazines
belongs_to :company
end
class Book < ActiveRecord::Base
belongs_to :author
belongs_to :publisher
accepts_nested_attributes_for :publisher
end
class Novel < Book
end
class Movie < ActiveRecord::Base
belongs_to :author
end
class Publisher < ActiveRecord::Base
has_many :books
end
class Profile < ActiveRecord::Base
belongs_to :author
has_one :profile_history
accepts_nested_attributes_for :profile_history
end
class ProfileHistory < ActiveRecord::Base
belongs_to :profile
end
class Magazine < ActiveRecord::Base
has_and_belongs_to_many :authors
end
class Company < ActiveRecord::Base
has_many :authors
end

# helpers
Expand Down Expand Up @@ -93,6 +119,31 @@ def error
"ERROR"
end
end
module PublisherDecorator
def upcased_name
name.upcase
end
end
module ProfileDecorator
def address
"secret"
end
end
module ProfileHistoryDecorator
def update_date
updated_on.strftime('%Y/%m/%d')
end
end
module MagazineDecorator
def upcased_title
title.upcase
end
end
module CompanyDecorator
def reverse_name
name.reverse
end
end

# decorator fake
class MovieDecorator; end
Expand Down Expand Up @@ -178,9 +229,15 @@ def thanks(book)
# migrations
class CreateAllTables < ActiveRecord::Migration
def self.up
create_table(:authors) {|t| t.string :name}
create_table(:books) {|t| t.string :title; t.references :author; t.string :type }
create_table(:movies) {|t| t.string :name}
create_table(:authors) {|t| t.string :name; t.references :company}
create_table(:books) {|t| t.string :title; t.string :type; t.references :author; t.references :publisher }
create_table(:profiles) {|t| t.string :address; t.references :author}
create_table(:profile_histories) {|t| t.date :updated_on; t.references :profile}
create_table(:publishers) {|t| t.string :name}
create_table(:movies) {|t| t.string :name; t.references :author}
create_table(:magazines) {|t| t.string :title}
create_table(:authors_magazines) {|t| t.references :author; t.references :magazine}
create_table(:companies) {|t| t.string :name}
end
end

Expand Down
36 changes: 36 additions & 0 deletions test/features/association_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'test_helper'

class AssociationTest < ActionDispatch::IntegrationTest
setup do
company = Company.create! name: 'NaCl'
@matz = company.authors.create! name: 'matz'
@matz.books.create!(
title: 'the world of code',
publisher_attributes: { name: 'nikkei linux' }
)
@matz.books.create!(
title: 'the ruby programming language',
publisher_attributes: { name: "o'reilly" }
)
@matz.create_profile! address: 'Matsue city, Shimane'
@matz.profile.create_profile_history! updated_on: Date.new(2017, 2, 7)
@matz.magazines.create! title: 'rubima'
end

test 'decorating associated objects' do
visit "/authors/#{@matz.id}"
assert page.has_content? 'the world of code'.upcase
assert page.has_content? 'the ruby programming language'.upcase
assert page.has_content? "o'reilly".upcase
assert page.has_content? 'secret'
assert page.has_content? '2017/02/07'
assert page.has_content? 'rubima'.upcase
assert page.has_content? 'NaCl'.reverse
end

test "decorating associated objects that owner doesn't have decorator" do
movie = Movie.create! author: @matz
visit "/movies/#{movie.id}"
assert page.has_content? 'matz'.reverse
end
end