Skip to content

Commit

Permalink
builders association reflector implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
miks committed Apr 20, 2015
1 parent 437e8c0 commit ea99750
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 117 deletions.
58 changes: 58 additions & 0 deletions releaf-core/app/builders/releaf/builders/association_reflector.rb
@@ -0,0 +1,58 @@
class Releaf::Builders::AssociationReflector
delegate :macro, :name, :klass, to: :reflection

attr_accessor :reflection, :fields, :sortable_column_name, :sortable_cache

def initialize(reflection, fields, sortable_column_name)
self.reflection = reflection
self.fields = fields
self.sortable_column_name = sortable_column_name.to_sym
end

def sortable?
if @sortable.nil?
@sortable = (expected_order_clause == actual_order_clause)
end

@sortable
end

def destroyable?
if @destroyable.nil?
@destroyable = reflection
.active_record
.nested_attributes_options
.fetch(reflection.name, {})
.fetch(:allow_destroy, false)
end

@destroyable
end

def actual_order_clause
relation = reflection.klass.all

if reflection.scope
relation = relation.instance_exec(reflection.active_record, &reflection.scope)
end

extract_order_clause(relation)
end

def expected_order_clause
relation = reflection.klass.all.order(sortable_column_name)
extract_order_clause(relation)
end

def extract_order_clause(relation)
relation.order_values.map{|value| value_as_sql(value) }.join(", ")
end

def value_as_sql(value)
if value.respond_to?(:to_sql)
value.to_sql
else
value
end
end
end
139 changes: 63 additions & 76 deletions releaf-core/app/builders/releaf/builders/form_builder.rb
Expand Up @@ -41,7 +41,6 @@ def normalize_field(field, subfields)
{
render_method: field_render_method_name(field),
field: field,
association: reflection(field).present?,
subfields: subfields
}
end
Expand All @@ -55,109 +54,114 @@ def releaf_fields(*fields)
def render_field_by_options(options)
if respond_to? options[:render_method]
send(options[:render_method])
elsif options[:association] == true
releaf_association_fields(options[:field], options[:subfields])
else
releaf_field(options[:field])
reflection = reflect_on_association(options[:field])

if reflection
releaf_association_fields(reflection, options[:subfields])
else
releaf_field(options[:field])
end
end
end

def reflection(reflection_name)
object.class.reflections[reflection_name.to_s]
def reflect_on_association(association_name)
object.class.reflections[association_name.to_s]
end

def association_reflector(reflection, fields)
fields ||= resource_fields.association_attributes(reflection)
Releaf::Builders::AssociationReflector.new(reflection, fields, sortable_column_name)
end

def releaf_association_fields(association_name, fields)
fields = association_fields(association_name) if fields.nil?
def releaf_association_fields(reflection, fields)
reflector = association_reflector(reflection, fields)

case reflection(association_name).macro
case reflector.macro
when :has_many
releaf_has_many_association(association_name, fields)
releaf_has_many_association(reflector)
when :belongs_to
releaf_belongs_to_association(association_name, fields)
releaf_belongs_to_association(reflector)
when :has_one
releaf_has_one_association(association_name, fields)
releaf_has_one_association(reflector)
else
raise 'not implemented'
end
end

def releaf_belongs_to_association(association_name, fields)
releaf_has_one_or_belongs_to_association(association_name, fields)
def releaf_belongs_to_association(reflector)
releaf_has_one_or_belongs_to_association(reflector)
end

def releaf_has_one_association(association_name, fields)
object.send("build_#{association_name}") unless object.send(association_name).present?
releaf_has_one_or_belongs_to_association(association_name, fields)
def releaf_has_one_association(reflector)
object.send("build_#{reflector.name}") unless object.send(reflector.name).present?
releaf_has_one_or_belongs_to_association(reflector)
end

def releaf_has_one_or_belongs_to_association(association_name, fields)
tag(:fieldset, class: "type-association", data: {name: association_name}) do
tag(:legend, translate_attribute(association_name)) <<
fields_for(association_name, object.send(association_name), relation_name: association_name, builder: self.class) do |builder|
builder.releaf_fields(fields)
def releaf_has_one_or_belongs_to_association(reflector)
tag(:fieldset, class: "type-association", data: {name: reflector.name}) do
tag(:legend, translate_attribute(reflector.name)) <<
fields_for(reflector.name, object.send(reflector.name), relation_name: reflector.name, builder: self.class) do |builder|
builder.releaf_fields(reflector.fields)
end
end
end

def releaf_has_many_association(association_name, fields)
item_template = releaf_has_many_association_item_template(association_name, fields)
def releaf_has_many_association(reflector)
item_template = releaf_has_many_association_fields(reflector, reflector.klass.new, '_template_', true)

tag(:section, class: "nested", data: {name: association_name, "releaf-template" => item_template}) do
[releaf_has_many_association_header(association_name),
releaf_has_many_association_body(association_name, fields),
releaf_has_many_association_footer(association_name)]
tag(:section, class: "nested", data: {name: reflector.name, "releaf-template" => html_escape(item_template.to_str)}) do
[
releaf_has_many_association_header(reflector),
releaf_has_many_association_body(reflector),
releaf_has_many_association_footer(reflector)
]
end
end

def releaf_has_many_association_item_template(association_name, fields)
reflection = reflection(association_name)
item_template = releaf_has_many_association_fields(association_name, obj: reflection.klass.new, child_index: '_template_', destroyable: true,
subfields: fields, sortable: sortable_association?(association_name))
item_template = html_escape(item_template.to_str) # make html unsafe and escape afterwards
end

def releaf_has_many_association_header(association_name)
def releaf_has_many_association_header(reflector)
tag(:header) do
tag(:h1, translate_attribute(association_name))
tag(:h1, translate_attribute(reflector.name))
end
end

def releaf_has_many_association_body(association_name, fields)
sortable = sortable_association?(association_name)
destroyable = destoyable_association?(association_name)
def releaf_has_many_association_body(reflector)
attributes = {
class: ["body", "list"]
}
attributes["data"] = {sortable: nil} if reflector.sortable?

tag(:div, class: "body list", data: {sortable: sortable ? '' : nil}) do
association_collection(association_name, sortable).each_with_index.map do |obj, i|
releaf_has_many_association_fields(association_name, obj: obj, child_index: i, destroyable: destroyable,
sortable: sortable, subfields: fields)
end
tag(:div, attributes) do
association_collection(reflector).each_with_index.map do |association_object, index|
releaf_has_many_association_fields(reflector, association_object, index, reflector.destroyable?)
end
end
end

def releaf_has_many_association_footer(association_name)
tag(:footer) do
field_type_add_nested
end
def releaf_has_many_association_footer(reflector)
tag(:footer){ field_type_add_nested }
end

def releaf_has_many_association_fields(association_name, obj: nil, subfields: nil, child_index: nil, destroyable: nil, sortable: nil)
tag(:fieldset, class: ["item", "type-association"], data: {name: association_name, index: child_index}) do
fields_for(association_name, obj, relation_name: association_name, child_index: child_index, builder: self.class) do |builder|
builder.releaf_has_many_association_field(association_name, sortable, subfields, destroyable)
def releaf_has_many_association_fields(reflector, association_object, association_index, destroyable)
tag(:fieldset, class: ["item", "type-association"], data: {name: reflector.name, index: association_index}) do
fields_for(reflector.name, association_object, relation_name: reflector.name,
child_index: association_index, builder: self.class) do |builder|
builder.releaf_has_many_association_field(reflector, destroyable)
end
end
end

def releaf_has_many_association_field(field, sortable, subfields, destroyable)
def releaf_has_many_association_field(reflector, destroyable)
content = ActiveSupport::SafeBuffer.new
skippable_fields = []

if sortable
subfields -= [sortable_column_name]
if reflector.sortable?
skippable_fields << sortable_column_name
content << hidden_field(sortable_column_name.to_sym, class: "item-position")
content << tag(:div, "&nbsp;".html_safe, class: "handle")
end

content << releaf_fields(subfields)
content << releaf_fields(reflector.fields - skippable_fields)
content << field_type_remove_nested if destroyable

content
Expand Down Expand Up @@ -582,25 +586,8 @@ def object_translation_scope
"activerecord.attributes.#{object.class.name.underscore}"
end

def association_fields(association_name)
resource_fields.association_attributes(reflection(association_name))
end

def sortable_association?(association_name)
reflection = reflection(association_name)
reflection.klass.column_names.include?(sortable_column_name)
end

def destoyable_association?(association_name)
reflection = reflection(association_name)
reflection.active_record.nested_attributes_options.fetch(reflection.name, {}).fetch(:allow_destroy, false)
end

def association_collection(association_name, sortable)
reflection = reflection(association_name)
collection = object.send(reflection.name)
collection = collection.reorder(sortable_column_name => :asc) if sortable
collection
def association_collection(reflector)
object.send(reflector.name)
end

def sortable_column_name
Expand Down
2 changes: 1 addition & 1 deletion releaf-core/lib/generators/dummy/templates/models/book.rb
@@ -1,6 +1,6 @@
class Book < ActiveRecord::Base
belongs_to :author
has_many :chapters, inverse_of: :book
has_many :chapters, -> { order(:item_position) }, inverse_of: :book
has_many :book_sequels, dependent: :destroy
has_many :sequels, through: :book_sequels

Expand Down
124 changes: 124 additions & 0 deletions releaf-core/spec/builders/builders/association_reflector_spec.rb
@@ -0,0 +1,124 @@
require 'spec_helper'

describe Releaf::Builders::AssociationReflector, type: :class do
let(:reflection){ Book.reflect_on_association("chapters") }
subject{ described_class.new(reflection, :b, "xx") }

describe "#initialize" do
it "assigns given reflection" do
expect(subject.reflection).to eq(reflection)
end

it "assigns given fields" do
expect(subject.fields).to eq(:b)
end

it "normalizes to symbol and assigns given sortable name" do
expect(subject.sortable_column_name).to eq(:xx)
end
end

describe "#destroyable?" do
context "when reflection allow to destroy through nested attributes" do
it "returns true" do
subject.reflection = Book.reflect_on_association("book_sequels")
expect(subject.destroyable?).to be true
end
end

context "when reflection does not allow to destroy through nested attributes" do
it "returns false" do
expect(subject.destroyable?).to be false
end
end

it "caches result" do
expect(subject.reflection.active_record).to receive(:nested_attributes_options).and_call_original.once
subject.destroyable?
subject.destroyable?
subject.destroyable?
end
end

describe "#sortable?" do
context "when expected and actual order clauses are same" do
it "returns true" do
allow(subject).to receive(:expected_order_clause).and_return(:a)
allow(subject).to receive(:actual_order_clause).and_return(:a)
expect(subject.sortable?).to be true
end
end

context "when expected and actual order clauses differs" do
it "returns false" do
allow(subject).to receive(:expected_order_clause).and_return(:a)
allow(subject).to receive(:actual_order_clause).and_return(:b)
expect(subject.sortable?).to be false
end
end

it "caches result" do
expect(subject).to receive(:expected_order_clause).and_return(:x).once
expect(subject).to receive(:actual_order_clause).and_return(:y).once
subject.sortable?
subject.sortable?
subject.sortable?
end
end

describe "#actual_order_clause" do
context "when scope exists within reflection" do
it "returns actual reflection klass order clause with evaluated scope" do
relation = reflection.klass.all.order(:item_position)
allow(subject).to receive(:extract_order_clause).with(relation).and_return(:y)
expect(subject.actual_order_clause).to eq(:y)
end
end

context "when no scope exists within reflection" do
it "returns actual reflection klass order clause" do
subject.reflection = Book.reflect_on_association("sequels")
relation = subject.reflection.klass.all
allow(subject).to receive(:extract_order_clause).with(relation).and_return(:y)
expect(subject.actual_order_clause).to eq(:y)
end
end
end

describe "#expected_order_clause" do
it "returns expected reflection klass order clause for sortable column name" do
subject.sortable_column_name = :title
relation = reflection.klass.all.order(:title)
allow(subject).to receive(:extract_order_clause).with(relation).and_return(:y)
expect(subject.expected_order_clause).to eq(:y)
end
end

describe "#extract_order_clause" do
it "returns order clauses normalized to string for given relation" do
relation = Book.order(genre: :desc)
expect(subject.extract_order_clause(relation)).to eq("`books`.`genre` DESC")
end

context "when relation has no order clauses" do
it "returns empty string" do
relation = Book.all
expect(subject.extract_order_clause(relation)).to eq("")
end
end
end

describe "#value_as_sql" do
context "when given value respond to sql" do
it "return resulting sql" do
expect(subject.value_as_sql(Book.all)).to eq("SELECT `books`.* FROM `books`")
end
end

context "when given value does not respond to sql" do
it "return given value" do
expect(subject.value_as_sql(12)).to eq(12)
end
end
end
end

0 comments on commit ea99750

Please sign in to comment.