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 | |