first implementation of the has_many association for content entries

did committed Mar 03, 2015
commit 0b557ff1d937a6dbb8d1e7fe0736cfa670c7ad23
Showing 9 changed files with 123 additions and 117 deletions
locomotive/steam/adapters/mongodb/query.rb b/lib/locomotive/steam/adapters/mongodb/query.rb +0 -10
@@ @@ -65,16 +65,6 @@ module Locomotive::Steam
where(site_id: @scope.site._id) if @scope.site
end
- # def resolve_key(key)
- # return key unless key.respond_to?(:include?)
- # if key.include?('.')
- # name, operator = key.split('.')
- # name.to_sym.send(operator.to_sym)
- # else
- # key
- # end
- # end
-
end
end
locomotive/steam/entities/content_type.rb b/lib/locomotive/steam/entities/content_type.rb +1 -1
@@ @@ -5,7 +5,7 @@ module Locomotive::Steam
include Locomotive::Steam::Models::Entity
extend Forwardable
- def_delegators :fields, :localized_fields_names, :belongs_to_fields
+ def_delegators :fields, :localized_fields_names, :belongs_to_fields, :has_many_fields
def initialize(attributes = {})
super({
locomotive/steam/models.rb b/lib/locomotive/steam/models.rb +1 -0
@@ @@ -2,6 +2,7 @@ require_relative 'models/concerns/validation'
require_relative 'models/i18n_field'
require_relative 'models/associations/embedded'
require_relative 'models/associations/belongs_to'
+ require_relative 'models/associations/has_many'
require_relative 'models/entity'
require_relative 'models/mapper'
require_relative 'models/scope'
locomotive/steam/models/associations/has_many.rb b/lib/locomotive/steam/models/associations/has_many.rb +19 -22
@@ @@ -9,42 +9,39 @@ module Locomotive::Steam
attr_reader :repository
- def initialize(repository_klass, scope, adapter)
+ def initialize(repository_klass, scope, adapter, options = {}, &block)
+ # build a new instance of the target repository
@repository = repository_klass.new(adapter)
# Note: if we change the locale of the parent repository, that won't
# reflect in that repository
@repository.scope = scope.dup
- end
- def set_condition
- @repository.association_condition = { }
- end
+ # the block will executed when a method of the target will be called
+ @block = block_given? ? block : nil
- def method_missing(name, *args, &block)
- @repository.send(name, *args, &block)
+ @options = options
end
- # include Morphine
+ def attach(name, entity)
+ @name, @entity = name, entity
+ end
- # # use the scope from the parent repository
- # # one of the benefits is that if we change the current_locale
- # # of the parent repository, that will change the local repository
- # # as well.
- # def initialize(repository_klass, collection, scope)
- # adapter.collection = collection
+ def target_key
+ :"#{@options[:inverse_of]}_id"
+ end
- # @repository = repository_klass.new(adapter)
- # @repository.scope = scope
- # end
+ def entity_id
+ @repository.i18n_value_of(@entity, @repository.identifier_name)
+ end
- # # In order to keep track of the entity which owns
- # # the association.
- # def attach(name, entity)
- # @repository.send(:"#{name}=", entity)
- # end
+ def method_missing(name, *args, &block)
+ @block.call(@repository) if @block
+ @repository.local_conditions[target_key] = entity_id
+ @repository.send(name, *args, &block)
+ end
end
locomotive/steam/models/mapper.rb b/lib/locomotive/steam/models/mapper.rb +20 -1
@@ @@ -10,7 +10,7 @@ module Locomotive::Steam
@localized_attributes = []
@default_attributes = []
- @associations = { embedded: [], belongs_to: [] }
+ @associations = { embedded: [], belongs_to: [], has_many: [] }
instance_eval(&block) if block_given?
end
@@ @@ -27,6 +27,10 @@ module Locomotive::Steam
@associations[:belongs_to] += [[name.to_sym, repository_klass, block]]
end
+ def has_many_association(name, repository_klass, options = {}, &block)
+ @associations[:has_many] += [[name.to_sym, repository_klass, options, block]]
+ end
+
def embedded_association(name, repository_klass)
@associations[:embedded] += [[name.to_sym, repository_klass]]
end
@@ @@ -35,6 +39,7 @@ module Locomotive::Steam
entity_klass.new(serialize(attributes)).tap do |entity|
attach_entity_to_embedded_associations(entity)
attach_entity_to_belongs_to_associations(entity)
+ attach_entity_to_has_many_associations(entity)
set_default_attributes(entity)
end
end
@@ @@ -44,6 +49,7 @@ module Locomotive::Steam
serialize_embedded_associations(attributes)
serialize_belongs_to_associations(attributes)
+ serialize_has_many_associations(attributes)
attributes
end
@@ @@ -80,6 +86,13 @@ module Locomotive::Steam
end
end
+ # build the has_many associations
+ def serialize_has_many_associations(attributes)
+ @associations[:has_many].each do |(name, repository_klass, options, block)|
+ attributes[name] = HasManyAssociation.new(repository_klass, @repository.scope, @repository.adapter, options, &block)
+ end
+ end
+
def attach_entity_to_embedded_associations(entity)
@associations[:embedded].each do |(name, _)|
key = self.name.to_s.singularize.to_sym
@@ @@ -93,6 +106,12 @@ module Locomotive::Steam
end
end
+ def attach_entity_to_has_many_associations(entity)
+ @associations[:has_many].each do |(name, _)|
+ entity[name].attach(name, entity)
+ end
+ end
+
def set_default_attributes(entity)
@default_attributes.each do |(name, value)|
_value = value.respond_to?(:call) ? value.call(@repository) : value
locomotive/steam/models/repository.rb b/lib/locomotive/steam/models/repository.rb +8 -0
@@ @@ -41,6 +41,14 @@ module Locomotive::Steam
adapter.key(name, operator)
end
+ def identifier_name
+ if adapter.respond_to?(:identifier_name)
+ adapter.identifier_name(mapper)
+ else
+ :_id
+ end
+ end
+
alias :all :query
def mapper(memoized = true)
locomotive/steam/repositories/content_entry_repository.rb b/lib/locomotive/steam/repositories/content_entry_repository.rb +18 -0
@@ @@ -89,6 +89,7 @@ module Locomotive
super(memoized).tap do |mapper|
add_localized_fields_to_mapper(mapper)
add_belongs_to_fields_to_mapper(mapper)
+ add_has_many_fields_to_mapper(mapper)
end
end
@@ @@ -115,6 +116,23 @@ module Locomotive
end
end
+ def add_has_many_fields_to_mapper(mapper)
+ self.content_type.has_many_fields.each do |field|
+ mapper.has_many_association(field.name, self.class, inverse_of: field.inverse_of) do |repository|
+ # Note: this code will be executed only when the attribute will be called
+
+ # load the target content type
+ _content_type = content_type_repository.find(field.target_id)
+
+ # the target repository uses this content type for all the other inner calls
+ repository.with(_content_type)
+
+ # the content type repository is also need by the target repository
+ repository.content_type_repository = content_type_repository
+ end
+ end
+ end
+
def prepare_conditions(*conditions)
[*conditions].inject({}) do |memo, hash|
memo.merge!(hash) unless hash.blank?
locomotive/steam/repositories/content_type_field_repository.rb b/lib/locomotive/steam/repositories/content_type_field_repository.rb +4 -0
@@ @@ -29,6 +29,10 @@ module Locomotive
query { where(type: :belongs_to) }.all
end
+ def has_many
+ query { where(type: :has_many) }.all
+ end
+
def localized_names
query { where(localized: true) }.all.map(&:name)
end
spec/unit/repositories/content_entry_repository_spec.rb +52 -83
@@ @@ -4,7 +4,7 @@ require_relative '../../../lib/locomotive/steam/adapters/filesystem.rb'
describe Locomotive::Steam::ContentEntryRepository do
- let(:type) { instance_double('Articles', _id: 1, slug: 'articles', order_by: nil, label_field_name: :title, localized_fields_names: [:title], belongs_to_fields: [], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
+ let(:type) { build_content_type('Articles', label_field_name: :title, localized_fields_names: [:title], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
let(:entries) { [{ content_type_id: 1, _position: 0, _label: 'Update #1', title: { fr: 'Mise a jour #1' }, text: { en: 'added some free stuff', fr: 'phrase FR' }, date: '2009/05/12', category: 'General' }] }
let(:locale) { :en }
let(:site) { instance_double('Site', _id: 1, default_locale: :en, locales: %i(en fr)) }
@@ @@ -21,9 +21,9 @@ describe Locomotive::Steam::ContentEntryRepository do
describe 'belongs_to' do
let(:field) { instance_double('Field', name: :author, type: :belongs_to, target_id: 2) }
- let(:type) { instance_double('Articles', _id: 1, slug: 'articles', order_by: nil, label_field_name: :title, belongs_to_fields: [field], fields_by_name: { title: instance_double('Field', name: :title, type: :string), author: field }, localized_fields_names: []) }
+ let(:type) { build_content_type('Articles', label_field_name: :title, belongs_to_fields: [field]) }
let(:entries) { [{ content_type_id: 1, title: 'Hello world', author_id: 'john-doe' }] }
- let(:other_type) { instance_double('Authors', _id: 2, slug: 'authors', order_by: nil, label_field_name: :name, belongs_to_fields: [], fields_by_name: { name: instance_double('Field', name: :name, type: :string) }, localized_fields_names: []) }
+ let(:other_type) { build_content_type('Authors', _id: 2, label_field_name: :name, fields_by_name: { name: instance_double('Field', name: :name, type: :string) }) }
let(:other_entries) { [{ content_type_id: 2, _slug: 'john-doe', name: 'John Doe' }] }
let(:type_repository) { instance_double('ContentTypeRepository', belongs_to: [field]) }
@@ @@ -45,6 +45,39 @@ describe Locomotive::Steam::ContentEntryRepository do
end
+ describe 'has_many' do
+
+ let(:field) { instance_double('Field', name: :articles, type: :has_many, target_id: 2, inverse_of: :author) }
+ let(:type) { build_content_type('Authors', label_field_name: :name, has_many_fields: [field]) }
+ let(:entries) { [{ content_type_id: 1, _id: 1, name: 'John Doe' }] }
+ let(:other_type) { build_content_type('Articles', _id: 2, label_field_name: :title, fields_by_name: { name: instance_double('Field', name: :title, type: :string) }) }
+ let(:other_entries) {
+ [
+ { content_type_id: 2, _slug: 'hello-world', title: 'Hello world', author_id: 'john-doe' },
+ { content_type_id: 2, _slug: 'lorem-ipsum', title: 'Lorem ipsum', author_id: 'john-doe' },
+ { content_type_id: 2, _slug: 'lost', title: 'Lost', author_id: 'jane-doe' },
+ ]
+ }
+
+ let(:type_repository) { instance_double('ContentTypeRepository', has_many: [field]) }
+
+ before do
+ allow(type).to receive(:fields).and_return(type_repository)
+ allow(content_type_repository).to receive(:find).with(2).and_return(other_type)
+ end
+
+ subject { repository.with(type).by_slug('john-doe') }
+
+ it { expect(subject.articles.class).to eq Locomotive::Steam::Models::HasManyAssociation }
+
+ it 'calls the new repository to fetch the target entities' do
+ articles = subject.articles
+ allow(adapter).to receive(:collection).and_return(other_entries)
+ expect(articles.all.map(&:title)).to eq ['Hello world', 'Lorem ipsum']
+ end
+
+ end
+
describe '#all' do
let(:conditions) { nil }
@@ @@ -77,21 +110,6 @@ describe Locomotive::Steam::ContentEntryRepository do
end
- # describe '#persist' do
-
- # # let(:entry) { instance_double('NewEntry', _visible: true, content_type: type, _label: 'Hello world', attributes: { title: 'Hello world' }) }
- # # subject { repository.persist(entry) }
-
- # # before do
- # # expect(entry).to receive(:[]).with(:_slug).and_return(nil)
- # # expect(entry).to receive(:[]=).with(:_slug, 'hello-world')
- # # expect(loader).to receive(:write).with(type, { title: 'Hello world' })
- # # end
-
- # # it { expect { subject }.to change { repository.all(type).size }.by(1) }
-
- # end
-
describe '#exists?' do
let(:conditions) { {} }
@@ @@ -129,71 +147,9 @@ describe Locomotive::Steam::ContentEntryRepository do
end
- # describe '#value_for' do
-
- # let(:name) { :title }
- # let(:entry) { instance_double('Article', title: 'Hello world') }
-
- # subject { repository.value_for(name, entry) }
-
- # it { is_expected.to eq 'Hello world' }
-
- # describe 'association do' do
-
- # let(:author_type) { instance_double('AuthorType') }
- # let(:entry) { instance_double('Article', _slug: 'hello-world', author: association, authors: association) }
-
- # before do
- # allow(content_type_repository).to receive(:by_slug).with(:authors).and_return(:author_type)
- # end
-
- # context 'belongs_to association' do
-
- # let(:association) { instance_double('Association', type: :belongs_to, association: true, target_class_slug: :authors, target_slugs: ['john-doe'], order_by: nil) }
- # let(:name) { :author }
-
- # before do
- # expect(repository).to receive(:by_slug).with(:author_type, 'john-doe').and_return('John Doe')
- # end
-
- # it { expect(subject).to eq 'John Doe' }
-
- # end
-
- # # context 'has_many association' do
-
- # # let(:association) { instance_double('Association', type: :has_many, association: true, target_class_slug: :authors, target_field: :article, order_by: 'created_at') }
- # # let(:name) { :authors }
-
- # # before do
- # # allow(association).to receive(:source).and_return(entry)
- # # expect(repository).to receive(:all).with(:author_type, { article: 'hello-world', order_by: 'created_at' }).and_return(%w(jane john))
- # # end
-
- # # it { expect(subject).to eq %w(jane john) }
-
- # # end
-
- # # context 'many_to_many association' do
-
- # # let(:association) { instance_double('Association', type: :many_to_many, association: true, target_class_slug: :authors, target_slugs: %w(jane john), order_by: nil) }
- # # let(:name) { :authors }
-
- # # before do
- # # expect(repository).to receive(:all).with(:author_type, { '_slug.in' => %w(jane john) }).and_return(%w(jane john))
- # # end
-
- # # it { expect(subject).to eq %w(jane john) }
-
- # # end
-
- # end
-
- # end
-
describe '#next' do
- let(:type) { instance_double('Articles', _id: 1, slug: 'articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title], belongs_to_fields: [], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
+ let(:type) { build_content_type('Articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
let(:entries) do
[
{ content_type_id: 1, _position: 0, _label: 'Update #1', title: { fr: 'Mise a jour #1' }, text: { en: 'added some free stuff', fr: 'phrase FR' }, date: '2009/05/12', category: 'General' },
@@ @@ -225,7 +181,7 @@ describe Locomotive::Steam::ContentEntryRepository do
describe '#previous' do
- let(:type) { instance_double('Articles', _id: 1, slug: 'articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title], belongs_to_fields: [], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
+ let(:type) { build_content_type('Articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title], fields_by_name: { title: instance_double('Field', name: :title, type: :string) }) }
let(:entries) do
[
{ content_type_id: 1, _position: 0, _label: 'Update #1', title: { fr: 'Mise a jour #1' }, text: { en: 'added some free stuff', fr: 'phrase FR' }, date: '2009/05/12', category: 'General' },
@@ @@ -272,7 +228,7 @@ describe Locomotive::Steam::ContentEntryRepository do
category: instance_double('SelectField', name: :category, type: :select, select_options: { en: ['cooking', 'bread'], fr: ['cuisine', 'pain'] })
}
end
- let(:type) { instance_double('Articles', _id: 1, slug: 'articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title, :category], belongs_to_fields: [], fields_by_name: fields) }
+ let(:type) { build_content_type('Articles', order_by: '_position asc', label_field_name: :title, localized_fields_names: [:title, :category], fields_by_name: fields) }
let(:name) { :category }
let(:entries) do
@@ @@ -294,4 +250,17 @@ describe Locomotive::Steam::ContentEntryRepository do
end
+ def build_content_type(name, attributes = {})
+ instance_double(name,
+ {
+ _id: 1,
+ slug: name.to_s.downcase,
+ order_by: nil,
+ localized_fields_names: [],
+ belongs_to_fields: [],
+ has_many_fields: [],
+ fields_by_name: {}
+ }.merge(attributes))
+ end
+
end