fix issue locomotivecms/wagon#270: with_scope doesn't work with many_to_many relationship field type
did
committed Nov 27, 2015
commit 0aa777b6ee89cf766b69c3e7234810fa75c08e01
Showing 5
changed files with
134 additions
and 42 deletions
locomotive/steam/liquid/tags/with_scope.rb b/lib/locomotive/steam/liquid/tags/with_scope.rb
+11
-9
| @@ | @@ -45,18 +45,20 @@ module Locomotive |
| # _slug instead of _permalink | |
| _key = key.to_s == '_permalink' ? '_slug' : key.to_s | |
| - | hash[_key] = (case value |
| - | # regexp inside a string |
| - | when /^\/[^\/]*\/$/ then Regexp.new(value[1..-2]) |
| - | # content entry drop? Use the source (entity) instead |
| - | when Locomotive::Steam::Liquid::Drops::ContentEntry |
| - | value.send(:_source) |
| - | else |
| - | value |
| - | end) |
| + | hash[_key] = cast_value(value) |
| end | |
| end | |
| end | |
| + | |
| + | def cast_value(value) |
| + | case value |
| + | when Array then value.map { |_value| cast_value(_value) } |
| + | when /^\/[^\/]*\/$/ then Regexp.new(value[1..-2]) |
| + | else |
| + | value.respond_to?(:_id) ? value.send(:_source) : value |
| + | end |
| + | end |
| + | |
| end | |
| end | |
locomotive/steam/repositories/content_entry_repository.rb b/lib/locomotive/steam/repositories/content_entry_repository.rb
+33
-29
| @@ | @@ -130,33 +130,11 @@ module Locomotive |
| end | |
| def prepare_conditions(*conditions) | |
| - | # _conditions = conditions.first.try(:with_indifferent_access) |
| - | |
| _conditions = Conditions.new(conditions.first, self.content_type.fields).prepare | |
| super({ _visible: true }, _conditions) | |
| end | |
| - | # # belongs_to fields? if so, make sure we use the _id and we deal with the ID, not the object itself |
| - | # def prepare_conditions_for_belongs_to_fields(conditions) |
| - | # self.content_type.fields.belongs_to.each do |field| |
| - | # if value = conditions[name = field.name.to_s] |
| - | # conditions.delete(name) |
| - | # conditions[name + '_id'] = value.try(:_id) |
| - | # end |
| - | # end |
| - | # end |
| - | |
| - | # # select fields? if so, use the _id of the option instead of the option name |
| - | # def prepare_conditions_for_select_fields(conditions) |
| - | # self.content_type.fields.selects.each do |field| |
| - | # if value = conditions[name = field.name.to_s] |
| - | # conditions.delete(name) |
| - | # conditions[name + '_id'] = field.select_options.by_name(value).try(:_id) |
| - | # end |
| - | # end |
| - | # end |
| - | |
| def add_localized_fields_to_mapper(mapper) | |
| unless self.content_type.localized_names.blank? | |
| mapper.localized_attributes(*self.content_type.localized_names) | |
| @@ | @@ -207,17 +185,25 @@ module Locomotive |
| class Conditions | |
| def initialize(conditions = {}, fields) | |
| - | @conditions, @fields = conditions.try(:with_indifferent_access) || {}, fields |
| + | @conditions, @fields, @operators = conditions.try(:with_indifferent_access) || {}, fields, {} |
| + | |
| + | @conditions.each do |name, value| |
| + | _name, operator = name.to_s.split('.') |
| + | @operators[_name] = operator if operator |
| + | end |
| end | |
| def prepare | |
| - | return {} if @conditions.blank? |
| - | |
| + | # selects |
| _prepare(@fields.selects) do |field, value| | |
| field.select_options.by_name(value).try(:_id) | |
| end | |
| - | _prepare(@fields.belongs_to) { |field, value| value.try(:_id) } |
| + | # belongs_to |
| + | _prepare(@fields.belongs_to) { |field, value| value_to_id(value) } |
| + | |
| + | # many_to_many |
| + | _prepare(@fields.many_to_many) { |field, value| values_to_ids(value) } |
| @conditions | |
| end | |
| @@ | @@ -226,13 +212,31 @@ module Locomotive |
| def _prepare(fields, &block) | |
| fields.each do |field| | |
| - | if value = @conditions[name = field.name.to_s] |
| - | @conditions.delete(name) |
| - | @conditions[name + '_id'] = yield(field, value) |
| + | name = field.name.to_s |
| + | operator = @operators[name] |
| + | _name = operator ? "#{name}.#{operator}" : name |
| + | |
| + | if value = @conditions[_name] |
| + | # delete old name |
| + | @conditions.delete(_name) |
| + | |
| + | # build the new name with the prefix and the operator if there is one |
| + | _name = field.persisted_name + (operator ? ".#{operator}" : '') |
| + | |
| + | # store the new name |
| + | @conditions[_name] = yield(field, value) |
| end | |
| end | |
| end | |
| + | def value_to_id(value) |
| + | value.respond_to?(:_id) ? value._id : value |
| + | end |
| + | |
| + | def values_to_ids(value) |
| + | [*value].map { |_value| value_to_id(_value) } |
| + | end |
| + | |
| end | |
| end | |
locomotive/steam/repositories/content_type_field_repository.rb b/lib/locomotive/steam/repositories/content_type_field_repository.rb
+4
-0
| @@ | @@ -26,6 +26,10 @@ module Locomotive |
| query { where(type: :belongs_to) }.all | |
| end | |
| + | def many_to_many |
| + | query { where(type: :many_to_many) }.all |
| + | end |
| + | |
| def associations | |
| query { where(k(:type, :in) => %i(belongs_to has_many many_to_many)) }.all | |
| end | |
spec/unit/liquid/tags/with_scope_spec.rb
+21
-0
| @@ | @@ -25,6 +25,27 @@ describe Locomotive::Steam::Liquid::Tags::WithScope do |
| end | |
| + | describe 'decode content entry' do |
| + | |
| + | let(:entry) { |
| + | instance_double('ContentEntry', _id: 1, _source: 'entity').tap do |_entry| |
| + | allow(_entry).to receive(:to_liquid).and_return(_entry) |
| + | end } |
| + | let(:assigns) { { 'my_project' => entry } } |
| + | let(:source) { "{% with_scope project: my_project %}{% assign conditions = with_scope %}{% endwith_scope %}" } |
| + | |
| + | it { expect(conditions['project']).to eq 'entity' } |
| + | |
| + | context 'an array of content entries' do |
| + | |
| + | let(:source) { "{% with_scope project: [my_project, my_project, my_project] %}{% assign conditions = with_scope %}{% endwith_scope %}" } |
| + | |
| + | it { expect(conditions['project']).to eq ['entity', 'entity', 'entity'] } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| describe 'decode context variable' do | |
| let(:assigns) { { 'params' => { 'type' => 'posts' } } } | |
spec/unit/repositories/content_entry_repository_spec.rb
+65
-4
| @@ | @@ -4,7 +4,7 @@ require_relative '../../../lib/locomotive/steam/adapters/filesystem.rb' |
| describe Locomotive::Steam::ContentEntryRepository do | |
| - | let(:_fields) { instance_double('Fields', selects: [], belongs_to: []) } |
| + | let(:_fields) { instance_double('Fields', selects: [], belongs_to: [], many_to_many: []) } |
| let(:type) { build_content_type('Articles', label_field_name: :title, localized_names: [:title], fields: _fields, 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 } | |
| @@ | @@ -248,7 +248,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| let(:other_type) { build_content_type('Authors', _id: 2, label_field_name: :name, fields: _fields, fields_by_name: { name: instance_double('Field', name: :name, type: :string) }) } | |
| let(:other_entries) { [{ content_type_id: 2, _id: 'john-doe', name: 'John Doe' }] } | |
| - | let(:type_repository) { instance_double('ArticleBelongsToRepository', selects: [], belongs_to: []) } |
| + | let(:type_repository) { instance_double('ArticleBelongsToRepository', selects: [], belongs_to: [], many_to_many: []) } |
| before do | |
| allow(type).to receive(:fields).and_return(type_repository) | |
| @@ | @@ -281,7 +281,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| ] | |
| } | |
| - | let(:type_repository) { instance_double('AuthorRepository', selects: [], belongs_to: []) } |
| + | let(:type_repository) { instance_double('AuthorRepository', selects: [], belongs_to: [], many_to_many: []) } |
| before do | |
| allow(type).to receive(:fields).and_return(type_repository) | |
| @@ | @@ -314,7 +314,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| ] | |
| } | |
| - | let(:type_repository) { instance_double('AuthorRepository', selects: [], belongs_to: []) } |
| + | let(:type_repository) { instance_double('AuthorRepository', selects: [], belongs_to: [], many_to_many: []) } |
| before do | |
| allow(type).to receive(:fields).and_return(type_repository) | |
| @@ | @@ -333,6 +333,67 @@ describe Locomotive::Steam::ContentEntryRepository do |
| end | |
| + | describe '#conditions_without_order_by' do |
| + | |
| + | let(:conditions) { {} } |
| + | |
| + | subject { repository.with(type).send(:conditions_without_order_by, conditions) } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1 }, nil]) } |
| + | |
| + | context 'select fields' do |
| + | |
| + | let(:value) { 'CMS' } |
| + | let(:option) { instance_double('Option', _id: 42)} |
| + | let(:options) { instance_double('OptionRepository', by_name: option) } |
| + | let(:field) { instance_double('SelectField', name: 'category', persisted_name: 'category_id', select_options: options) } |
| + | let(:_fields) { instance_double('Fields', selects: [field], belongs_to: [], many_to_many: []) } |
| + | let(:conditions) { { 'category' => value } } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1, 'category_id' => 42 }, nil]) } |
| + | |
| + | end |
| + | |
| + | context 'belongs_to fields' do |
| + | |
| + | let(:value) { 42 } |
| + | let(:field) { instance_double('BelongsToField', name: 'person', persisted_name: 'person_id') } |
| + | let(:_fields) { instance_double('Fields', selects: [], belongs_to: [field], many_to_many: []) } |
| + | let(:conditions) { { 'person' => value } } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1, 'person_id' => 42 }, nil]) } |
| + | |
| + | context 'the target value is a content entry' do |
| + | |
| + | let(:value) { instance_double('TargetContentEntry', _id: 1) } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1, 'person_id' => 1 }, nil]) } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | context 'many_to_many fields' do |
| + | |
| + | let(:value) { 42 } |
| + | let(:field) { instance_double('ManyToManyField', name: 'tags', persisted_name: 'tag_ids') } |
| + | let(:_fields) { instance_double('Fields', selects: [], belongs_to: [], many_to_many: [field]) } |
| + | let(:conditions) { { 'tags.in' => value } } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1, 'tag_ids.in' => [42] }, nil]) } |
| + | |
| + | context 'the target value is a content entry' do |
| + | |
| + | let(:value) { [instance_double('TargetContentEntry', _id: 1), 42] } |
| + | |
| + | it { is_expected.to eq([{ _visible: true, content_type_id: 1, 'tag_ids.in' => [1, 42] }, nil]) } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| def build_content_type(name, attributes = {}) | |
| instance_double(name, | |
| { | |