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