many_to_many association for content entries
did
committed Mar 04, 2015
commit bb1deb58e3143dd2c704e0becc48902cca3f00c4
Showing 9
changed files with
90 additions
and 56 deletions
locomotive/steam/adapters/filesystem/yaml_loaders/content_entry.rb b/lib/locomotive/steam/adapters/filesystem/yaml_loaders/content_entry.rb
+21
-8
| @@ | @@ -20,23 +20,36 @@ module Locomotive |
| each(content_type_slug) do |label, attributes, position| | |
| _attributes = { _position: position, _label: label.to_s }.merge(attributes) | |
| - | setup_belongs_to_associations(_attributes) |
| + | setup_associations(_attributes) |
| list << _attributes | |
| end | |
| end | |
| end | |
| - | def setup_belongs_to_associations(attributes) |
| - | content_type.belongs_to_fields.each do |field| |
| - | # <name>_id |
| - | attributes[:"#{field.name}_id"] = attributes.delete(field.name.to_sym) |
| - | |
| - | # _position_in_<name> |
| - | attributes[:"_position_in_#{field.name}"] = attributes[:_position] |
| + | def setup_associations(attributes) |
| + | content_type.association_fields.each do |field| |
| + | case field.type |
| + | when :belongs_to |
| + | setup_belongs_to_association(field, attributes) |
| + | when :many_to_many |
| + | setup_many_to_many_association(field, attributes) |
| + | end |
| end | |
| end | |
| + | def setup_belongs_to_association(field, attributes) |
| + | # <name>_id |
| + | attributes[:"#{field.name}_id"] = attributes.delete(field.name.to_sym) |
| + | |
| + | # _position_in_<name> |
| + | attributes[:"_position_in_#{field.name}"] = attributes[:_position] |
| + | end |
| + | |
| + | def setup_many_to_many_association(field, attributes) |
| + | attributes[:"#{field.name.to_s.singularize}_ids"] = attributes.delete(field.name.to_sym) |
| + | end |
| + | |
| def each(slug, &block) | |
| position = 0 | |
| _load(File.join(path, "#{slug}.yml")).each do |element| | |
locomotive/steam/models.rb b/lib/locomotive/steam/models.rb
+1
-0
| @@ | @@ -4,6 +4,7 @@ require_relative 'models/associations/embedded' |
| require_relative 'models/associations/referenced' | |
| require_relative 'models/associations/belongs_to' | |
| require_relative 'models/associations/has_many' | |
| + | require_relative 'models/associations/many_to_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
+2
-1
| @@ | @@ -6,7 +6,8 @@ module Locomotive::Steam |
| def __load__ | |
| # Note: in adapters like the FileSystem one, we use slugs | |
| - | # to reference other entities in associations. |
| + | # to reference other entities in associations, |
| + | # that is why we call identifier_name. |
| id = @repository.i18n_value_of(@entity, @repository.identifier_name) | |
| key = :"#{@options[:inverse_of]}_id" | |
locomotive/steam/models/associations/many_to_many.rb b/lib/locomotive/steam/models/associations/many_to_many.rb
+26
-0
| @@ | @@ -0,0 +1,26 @@ |
| + | module Locomotive::Steam |
| + | module Models |
| + | |
| + | # Note: represents an embedded collection |
| + | class ManyToManyAssociation < ReferencedAssociation |
| + | |
| + | def __load__ |
| + | # Note: in adapters like the FileSystem one, we use slugs |
| + | # to reference other entities in associations, |
| + | # that is why we call identifier_name. |
| + | source_key = :"#{@options[:association_name].to_s.singularize}_ids" |
| + | key = @repository.k(@repository.identifier_name, :in) |
| + | |
| + | @repository.local_conditions[key] = @entity[source_key] |
| + | |
| + | # use order_by from options as the default one for further queries |
| + | @repository.local_conditions[:order_by] = @options[:order_by] unless @options[:order_by].blank? |
| + | |
| + | # all the further calls (method_missing) will be delegated to @repository |
| + | @repository |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
locomotive/steam/models/mapper.rb b/lib/locomotive/steam/models/mapper.rb
+2
-1
| @@ | @@ -6,7 +6,8 @@ module Locomotive::Steam |
| ASSOCIATION_CLASSES = { | |
| embedded: EmbeddedAssociation, | |
| belongs_to: BelongsToAssociation, | |
| - | has_many: HasManyAssociation |
| + | has_many: HasManyAssociation, |
| + | many_to_many: ManyToManyAssociation |
| }.freeze | |
| attr_reader :name, :options, :default_attributes, :localized_attributes, :associations | |
locomotive/steam/repositories/content_entry_repository.rb b/lib/locomotive/steam/repositories/content_entry_repository.rb
+0
-33
| @@ | @@ -129,39 +129,6 @@ module Locomotive |
| first { where(conditions) } | |
| end | |
| - | # def association(metadata, conditions = {}) |
| - | # case metadata.type |
| - | # when :belongs_to then belongs_to_association(metadata) |
| - | # when :has_many then has_many_association(metadata, conditions) |
| - | # when :many_to_many then many_to_many_association(metadata, conditions) |
| - | # end |
| - | # end |
| - | |
| - | # def belongs_to_association(metadata) |
| - | # type = type_from(metadata.target_class_slug) |
| - | # by_slug(type, metadata.target_slugs.first) |
| - | # end |
| - | |
| - | # def has_many_association(metadata, conditions) |
| - | # many_association(metadata, |
| - | # { metadata.target_field => localized_slug(metadata.source) }.merge(conditions)) |
| - | # end |
| - | |
| - | # def many_to_many_association(metadata, conditions) |
| - | # many_association(metadata, |
| - | # { '_slug.in' => metadata.target_slugs }.merge(conditions)) |
| - | # end |
| - | |
| - | # def many_association(metadata, conditions) |
| - | # type = type_from(metadata.target_class_slug) |
| - | |
| - | # if order_by = metadata.order_by |
| - | # conditions = { order_by: order_by }.merge(conditions) |
| - | # end |
| - | |
| - | # all(type, conditions) |
| - | # end |
| - | |
| end | |
| end | |
locomotive/steam/repositories/content_type_field_repository.rb b/lib/locomotive/steam/repositories/content_type_field_repository.rb
+0
-8
| @@ | @@ -29,14 +29,6 @@ module Locomotive |
| query { where(required: true) }.all | |
| end | |
| - | # def belongs_to |
| - | # 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/adapters/filesystem/yaml_loaders/content_entry_spec.rb
+3
-3
| @@ | @@ -6,7 +6,7 @@ require_relative '../../../../../lib/locomotive/steam/adapters/filesystem/yaml_l |
| describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do | |
| let(:site_path) { default_fixture_site_path } | |
| - | let(:content_type) { instance_double('Bands', slug: 'bands', belongs_to_fields: []) } |
| + | let(:content_type) { instance_double('Bands', slug: 'bands', association_fields: []) } |
| let(:scope) { instance_double('Scope', locale: :en, context: { content_type: content_type }) } | |
| let(:loader) { described_class.new(site_path) } | |
| @@ | @@ -22,8 +22,8 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do |
| context 'a content type with a belongs_to field' do | |
| - | let(:field) { instance_double('Field', name: 'band') } |
| - | let(:content_type) { instance_double('Songs', slug: 'songs', belongs_to_fields: [field]) } |
| + | let(:field) { instance_double('Field', name: 'band', type: :belongs_to) } |
| + | let(:content_type) { instance_double('Songs', slug: 'songs', association_fields: [field]) } |
| it 'adds a new attribute for the foreign key' do | |
| expect(subject.first[:band_id]).to eq 'pearl-jam' | |
spec/unit/repositories/content_entry_repository_spec.rb
+35
-2
| @@ | @@ -198,7 +198,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| 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]) } |
| + | let(:type_repository) { instance_double('ContentTypeRepository') } |
| before do | |
| allow(type).to receive(:fields).and_return(type_repository) | |
| @@ | @@ -231,7 +231,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| ] | |
| } | |
| - | let(:type_repository) { instance_double('ContentTypeRepository', has_many: [field]) } |
| + | let(:type_repository) { instance_double('ContentTypeRepository') } |
| before do | |
| allow(type).to receive(:fields).and_return(type_repository) | |
| @@ | @@ -250,6 +250,39 @@ describe Locomotive::Steam::ContentEntryRepository do |
| end | |
| + | describe 'many_to_many' do |
| + | |
| + | let(:field) { instance_double('Field', name: :articles, type: :many_to_many, association_options: { target_id: 2, inverse_of: :authors }) } |
| + | let(:type) { build_content_type('Authors', label_field_name: :name, association_fields: [field]) } |
| + | let(:entries) { [{ content_type_id: 1, _id: 1, name: 'John Doe', article_ids: ['hello-world', 'lorem-ipsum'] }] } |
| + | 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', position_in_author: 2 }, |
| + | { content_type_id: 2, _slug: 'lorem-ipsum', title: 'Lorem ipsum', author_id: 'john-doe', position_in_author: 1 }, |
| + | { content_type_id: 2, _slug: 'lost', title: 'Lost', author_id: 'jane-doe' }, |
| + | ] |
| + | } |
| + | |
| + | let(:type_repository) { instance_double('ContentTypeRepository') } |
| + | |
| + | 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::ManyToManyAssociation } |
| + | |
| + | 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 |
| + | |
| def build_content_type(name, attributes = {}) | |
| instance_double(name, | |
| { | |