locomotive_cms 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,8 @@ module Locomotive
15
15
 
16
16
  before_filter :set_current_thread_variables
17
17
 
18
+ rescue_from Exception, with: :render_access_denied
19
+
18
20
  self.responder = Locomotive::ActionController::Responder # custom responder
19
21
 
20
22
  respond_to :json, :xml
@@ -41,6 +43,15 @@ module Locomotive
41
43
  self.setup_i18n_fallbacks
42
44
  end
43
45
 
46
+ def render_access_denied(exception)
47
+ status = (case exception
48
+ when ::CanCan::AccessDenied then 401
49
+ when ::Mongoid::Errors::DocumentNotFound then 404
50
+ else 500
51
+ end)
52
+ render json: { error: exception.message }, status: status, layout: false
53
+ end
54
+
44
55
  def self.cancan_resource_class
45
56
  Locomotive::Api::CanCan::ControllerResource
46
57
  end
@@ -38,7 +38,7 @@ module CustomFields
38
38
 
39
39
  # Set correct paths
40
40
  def store_dir
41
- "sites/#{model.site_id}/content_#{model.class.model_name.demodulize.underscore}/#{model.id}/files"
41
+ "sites/#{model.site_id}/#{model.class.model_name.demodulize.underscore}/#{model.id}/files"
42
42
  end
43
43
 
44
44
  # In some situations, for instance, for the notification email when a content entry is created,
@@ -7,18 +7,39 @@ module Locomotive
7
7
  end
8
8
 
9
9
  def call(env)
10
- path = env['PATH_INFO']
10
+ path, query = env['PATH_INFO'], env['QUERY_STRING']
11
11
 
12
12
  if !path.starts_with?("#{Locomotive.mounted_on}/") && (match = path.match(%r{(.+)/$}))
13
- response = Rack::Response.new
14
- response.redirect(match[1], 301) # moved permanently
15
- response.finish
16
- response.to_a
13
+ url = self.redirect_url(match[1], query)
14
+ self.redirect_to(url)
17
15
  else
18
16
  @app.call(env)
19
17
  end
20
18
  end
21
19
 
20
+ protected
21
+
22
+ # Create a 301 response and set it up accordingly.
23
+ #
24
+ # @params [ String ] url The url for the redirection
25
+ #
26
+ # @return [ Array ] It has the 3 parameters (status, header, body)
27
+ #
28
+ def redirect_to(url)
29
+ response = Rack::Response.new
30
+ response.redirect(url, 301) # moved permanently
31
+ response.finish
32
+ response.to_a
33
+ end
34
+
35
+ def redirect_url(base, query)
36
+ if query.blank?
37
+ base
38
+ else
39
+ "#{base}?#{query}"
40
+ end
41
+ end
42
+
22
43
  end
23
44
  end
24
45
  end
@@ -1,3 +1,3 @@
1
1
  module Locomotive #:nodoc
2
- VERSION = '2.0.1'
2
+ VERSION = '2.0.2'
3
3
  end
@@ -0,0 +1,107 @@
1
+ class MakeEditableElementsConsistent < MongoidMigration::Migration
2
+ def self.up
3
+ Locomotive::Site.all.each do |site|
4
+ site.locales.each do |locale|
5
+ ::Mongoid::Fields::I18n.locale = locale
6
+ site.pages.each do |page|
7
+ puts "[#{site.name}] #{page.fullpath} (#{locale})"
8
+
9
+ found_elements = []
10
+
11
+ page.template.walk do |node, memo|
12
+ case node
13
+ when Locomotive::Liquid::Tags::InheritedBlock
14
+ puts "found block ! #{node.name} --- #{memo[:parent_block_name]}"
15
+
16
+ # set the new name based on a potential parent block
17
+ name = node.name.gsub(/[\"\']/o, '')
18
+
19
+ if memo[:parent_block_name] && !name.starts_with?(memo[:parent_block_name])
20
+ name = "#{memo[:parent_block_name]}/#{name}"
21
+ end
22
+
23
+ puts "new_name = #{name}"
24
+
25
+ # retrieve all the editable elements of this block and set them the new name
26
+ page.find_editable_elements(node.name).each do |el|
27
+ # puts "**> hurray found the element #{el.block} _ #{el.slug}"
28
+ el.block = name
29
+ puts "**> hurray found the element #{el.block} _ #{el.slug} | #{page.find_editable_element(name, el.slug).present?.inspect}"
30
+ end
31
+
32
+ # assign the new name to the block
33
+ node.instance_variable_set :@name, name
34
+
35
+ # record the new parent block name for children
36
+ memo[:parent_block_name] = name
37
+
38
+ when Locomotive::Liquid::Tags::Editable::ShortText,
39
+ Locomotive::Liquid::Tags::Editable::LongText,
40
+ Locomotive::Liquid::Tags::Editable::Control,
41
+ Locomotive::Liquid::Tags::Editable::File
42
+
43
+ puts "\tfound editable_element ! #{node.slug} --- #{memo[:parent_block_name]}"
44
+
45
+ slug = node.slug.gsub(/[\"\']/o, '')
46
+
47
+ # assign the new slug to the editable element
48
+ puts "\t\t...looking for #{node.slug} inside #{memo[:parent_block_name]}"
49
+
50
+ options = node.instance_variable_get :@options
51
+ block = options[:block].blank? ? memo[:parent_block_name] : options[:block]
52
+
53
+ if el = page.find_editable_element(block, node.slug)
54
+ puts "\t\t--> yep found the element"
55
+
56
+ el.slug = slug
57
+ el.block = memo[:parent_block_name] # just to make sure
58
+
59
+ node.instance_variable_set :@slug, slug
60
+
61
+ options.delete(:block)
62
+ node.instance_variable_set :@block, nil # just to make sure
63
+
64
+ found_elements << el._id
65
+ else
66
+ puts "\t\t[WARNING] el not found (#{block} - #{slug})"
67
+ end
68
+
69
+ end
70
+
71
+ memo
72
+ end # page walk
73
+
74
+ puts "found elements = #{found_elements.join(', ')} / #{page.editable_elements.count}"
75
+
76
+ # "hide" useless editable elements
77
+ page.editable_elements.each do |el|
78
+ next if found_elements.include?(el._id)
79
+ el.disabled = true
80
+ end
81
+
82
+ # serialize
83
+ page.send(:_serialize_template)
84
+
85
+ # puts page.template.inspect
86
+
87
+ # save ?
88
+ page.instance_variable_set :@template_changed, false
89
+ page.save
90
+
91
+ # TODO:
92
+ # x ", block: 'Asset #1'"" ???? les re-assigner a "main" d'une facon ou d'une autre
93
+ # => en fait, ce sont des editable elements qui n'ont pas vrais blocks
94
+ # x hide useless editable elements
95
+ # x re-serializer le template
96
+ # ? skipper la methode parse (quoique pas besoin car template non modifie)
97
+ # x snippets
98
+ # x sauvegarder (sans callbacks ??)
99
+ end # loop: pages
100
+ end # loop: locales
101
+ end # loop: sites
102
+ end
103
+
104
+ def self.down
105
+ raise MongoidMigration::IrreversibleMigration
106
+ end
107
+ end
@@ -0,0 +1,107 @@
1
+ class RenameEntryToContentEntry < MongoidMigration::Migration
2
+
3
+ def self.up
4
+ regexp = /(^|Locomotive::)(Content)*Entry([0-9a-fA-F]{24})$/
5
+ replacement = "\\1ContentEntry\\3"
6
+
7
+ # content entries
8
+ self.update_content_entries(regexp, replacement)
9
+
10
+ # content types
11
+ self.update_content_types(regexp, replacement)
12
+
13
+ # templatized pages
14
+ self.update_templatized_pages(regexp, replacement)
15
+ end
16
+
17
+ def self.down
18
+ regexp = /(^|Locomotive::)(Content)+Entry([0-9a-fA-F]{24})$/
19
+ replacement = "\\1Entry\\3"
20
+
21
+ # content entries
22
+ self.update_content_entries(regexp, replacement)
23
+
24
+ # content types
25
+ self.update_content_types(regexp, replacement)
26
+
27
+ # templatized pages
28
+ self.update_templatized_pages(regexp, replacement)
29
+ end
30
+
31
+ private
32
+
33
+ def self.update_content_entries(regexp, replacement)
34
+ self.fetch_rows(Locomotive::ContentEntry) do |collection, attributes|
35
+ recipe = attributes['custom_fields_recipe']
36
+ type = attributes['_type']
37
+ new_recipe = replace_value_by(recipe, regexp, replacement)
38
+ selector = { '_id' => attributes['_id'] }
39
+ operations = { '$set' => {
40
+ '_type' => replace_value_by(type, regexp, replacement),
41
+ 'custom_fields_recipe' => new_recipe
42
+ } }
43
+ collection.update selector, operations
44
+ end
45
+ puts "content entries UPDATED"
46
+ end
47
+
48
+ def self.update_content_types(regexp, replacement)
49
+ self.fetch_rows(Locomotive::ContentType) do |collection, attributes|
50
+ updates = {}
51
+ attributes['entries_custom_fields'].each_with_index do |custom_field, index|
52
+ new_custom_field = replace_value_by(custom_field, regexp, replacement)
53
+ updates["entries_custom_fields.#{index}"] = new_custom_field
54
+ end
55
+
56
+ selector = { '_id' => attributes['_id'] }
57
+ operations = { '$set' => updates }
58
+ collection.update selector, operations
59
+ end
60
+ puts "content types UPDATED"
61
+ end
62
+
63
+ def self.update_templatized_pages(regexp, replacement)
64
+ self.fetch_rows(Locomotive::Page) do |collection, attributes|
65
+ if klass_name = attributes['target_klass_name']
66
+ new_klass_name = replace_value_by(klass_name, regexp, replacement)
67
+ selector = { '_id' => attributes['_id'] }
68
+ operations = { '$set' => { 'target_klass_name' => new_klass_name } }
69
+ collection.update selector, operations
70
+ end
71
+ end
72
+ puts "templatized pages UPDATED"
73
+ end
74
+
75
+ def self.fetch_rows(klass, &block)
76
+ per_page = 100
77
+ collection = klass.collection.driver
78
+ count = collection.count
79
+ num_pages = (count.to_f / per_page).floor
80
+
81
+ # paginate the whole collection to avoid mongodb cursor error
82
+ (0..num_pages).each do |page|
83
+ offset = per_page * page.to_i
84
+ collection.find.sort("_id").limit(per_page).skip(offset).each do |attributes|
85
+ block.call(collection, attributes)
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.replace_value_by(object, regexp, replacement)
91
+ return object.gsub(regexp, replacement) if object.is_a?(String)
92
+
93
+ list = nil
94
+ list = object if object.is_a?(Array)
95
+ list = object.values if object.is_a?(Hash)
96
+
97
+ list.each do |value|
98
+ case value
99
+ when String then value.gsub!(regexp, replacement)
100
+ when Array, Hash then self.replace_value_by(value, regexp, replacement)
101
+ end
102
+ end
103
+
104
+ object
105
+ end
106
+
107
+ end
@@ -7,10 +7,10 @@ describe Locomotive::ContentEntry do
7
7
  before(:each) do
8
8
  Locomotive::Site.any_instance.stubs(:create_default_pages!).returns(true)
9
9
  @content_type = FactoryGirl.build(:content_type)
10
- @content_type.entries_custom_fields.build :label => 'Title', :type => 'string'
11
- @content_type.entries_custom_fields.build :label => 'Description', :type => 'text'
12
- @content_type.entries_custom_fields.build :label => 'Visible ?', :type => 'boolean', :name => 'visible'
13
- @content_type.entries_custom_fields.build :label => 'File', :type => 'file'
10
+ @content_type.entries_custom_fields.build label: 'Title', type: 'string'
11
+ @content_type.entries_custom_fields.build label: 'Description', type: 'text'
12
+ @content_type.entries_custom_fields.build label: 'Visible ?', type: 'boolean', name: 'visible'
13
+ @content_type.entries_custom_fields.build label: 'File', type: 'file'
14
14
  @content_type.valid?
15
15
  @content_type.send(:set_label_field)
16
16
  end
@@ -24,56 +24,57 @@ describe Locomotive::ContentEntry do
24
24
  ## Validations ##
25
25
 
26
26
  it 'requires the presence of title' do
27
- content_entry = build_content_entry :title => nil
27
+ content_entry = build_content_entry title: nil
28
28
  content_entry.should_not be_valid
29
29
  content_entry.errors[:title].should == ["can't be blank"]
30
30
  end
31
31
 
32
32
  it 'requires the presence of the permalink (_slug)' do
33
- content_entry = build_content_entry :title => nil
33
+ content_entry = build_content_entry title: nil
34
34
  content_entry.should_not be_valid
35
35
  content_entry.errors[:_slug].should == ["can't be blank"]
36
36
  end
37
37
 
38
38
  end
39
39
 
40
- context 'setting the slug' do
40
+ describe '#slug' do
41
+
41
42
  before :each do
42
- build_content_entry(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs'
43
+ build_content_entry(_slug: 'dogs').tap(&:save!)._slug.should == 'dogs'
43
44
  end
44
45
 
45
46
  it 'uses the given slug if it is unique' do
46
- build_content_entry(:_slug => 'monkeys').tap(&:save!)._slug.should == 'monkeys'
47
- build_content_entry(:_slug => 'cats-2').tap(&:save!)._slug.should == 'cats-2'
47
+ build_content_entry(_slug: 'monkeys').tap(&:save!)._slug.should == 'monkeys'
48
+ build_content_entry(_slug: 'cats-2').tap(&:save!)._slug.should == 'cats-2'
48
49
  end
49
50
 
50
51
  it 'appends a number to the end of the slug if it is not unique' do
51
- build_content_entry(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-1'
52
- build_content_entry(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-2'
53
- build_content_entry(:_slug => 'dogs-2').tap(&:save!)._slug.should == 'dogs-3'
54
- build_content_entry(:_slug => 'dogs-2').tap(&:save!)._slug.should == 'dogs-4'
52
+ build_content_entry(_slug: 'dogs').tap(&:save!)._slug.should == 'dogs-1'
53
+ build_content_entry(_slug: 'dogs').tap(&:save!)._slug.should == 'dogs-2'
54
+ build_content_entry(_slug: 'dogs-2').tap(&:save!)._slug.should == 'dogs-3'
55
+ build_content_entry(_slug: 'dogs-2').tap(&:save!)._slug.should == 'dogs-4'
55
56
  end
56
57
 
57
58
  it 'ignores the case of a slug' do
58
- build_content_entry(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-1'
59
- build_content_entry(:_slug => 'DOGS').tap(&:save!)._slug.should == 'dogs-2'
59
+ build_content_entry(_slug: 'dogs').tap(&:save!)._slug.should == 'dogs-1'
60
+ build_content_entry(_slug: 'DOGS').tap(&:save!)._slug.should == 'dogs-2'
60
61
  end
61
62
 
62
63
  it 'correctly handles slugs with multiple numbers' do
63
- build_content_entry(:_slug => 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-2'
64
- build_content_entry(:_slug => 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-3'
64
+ build_content_entry(_slug: 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-2'
65
+ build_content_entry(_slug: 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-3'
65
66
 
66
- build_content_entry(:_slug => 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi'
67
- build_content_entry(:_slug => 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi-1'
67
+ build_content_entry(_slug: 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi'
68
+ build_content_entry(_slug: 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi-1'
68
69
  end
69
70
  end
70
71
 
71
- context '#i18n' do
72
+ describe '#i18n' do
72
73
 
73
74
  before(:each) do
74
75
  localize_content_type @content_type
75
76
  ::Mongoid::Fields::I18n.locale = 'en'
76
- @content_entry = build_content_entry(:title => 'Hello world')
77
+ @content_entry = build_content_entry(title: 'Hello world')
77
78
  ::Mongoid::Fields::I18n.locale = 'fr'
78
79
  end
79
80
 
@@ -90,12 +91,13 @@ describe Locomotive::ContentEntry do
90
91
  end
91
92
 
92
93
  describe "#navigation" do
94
+
93
95
  before(:each) do
94
96
  @content_type.order_by = '_position'
95
97
  @content_type.save
96
98
 
97
99
  %w(first second third).each_with_index do |item, index|
98
- content = build_content_entry(:title => item.to_s, :_position => index)
100
+ content = build_content_entry(title: item.to_s, _position: index)
99
101
  content.save
100
102
  instance_variable_set "@#{item}", content
101
103
  end
@@ -196,24 +198,35 @@ describe Locomotive::ContentEntry do
196
198
  end
197
199
 
198
200
  it 'uses the to_label method if the value of the label field defined it' do
199
- entry = build_content_entry(:_label_field_name => 'with_to_label')
200
- entry.stubs(:with_to_label).returns(mock('with_to_label', :to_label => 'acme'))
201
+ entry = build_content_entry(_label_field_name: 'with_to_label')
202
+ entry.stubs(:with_to_label).returns(mock('with_to_label', to_label: 'acme'))
201
203
  entry._label.should == 'acme'
202
204
  end
203
205
 
204
206
  it 'uses the to_s method at last if the label field did not define the to_label method' do
205
- entry = build_content_entry(:_label_field_name => 'not_a_string')
206
- entry.stubs(:not_a_string).returns(mock('not_a_string', :to_s => 'not_a_string'))
207
+ entry = build_content_entry(_label_field_name: 'not_a_string')
208
+ entry.stubs(:not_a_string).returns(mock('not_a_string', to_s: 'not_a_string'))
207
209
  entry._label.should == 'not_a_string'
208
210
  end
209
211
 
210
212
  end
211
213
 
214
+ describe '#file' do
215
+
216
+ let(:entry) { build_content_entry(title: 'Hello world', file: FixturedAsset.open('5k.png')) }
217
+
218
+ it 'writes the file to the filesystem' do
219
+ entry.save
220
+ entry.file.url.should_not =~ /content_content_entry/
221
+ end
222
+
223
+ end
224
+
212
225
  describe '#public_submission' do
213
226
 
214
227
  before(:each) do
215
- @account_1 = FactoryGirl.build('admin user', :id => fake_bson_id('1'))
216
- @account_2 = FactoryGirl.build('frenchy user', :id => fake_bson_id('2'))
228
+ @account_1 = FactoryGirl.build('admin user', id: fake_bson_id('1'))
229
+ @account_2 = FactoryGirl.build('frenchy user', id: fake_bson_id('2'))
217
230
 
218
231
  @content_type.public_submission_enabled = true
219
232
  @content_type.public_submission_accounts = ['', @account_1._id, @account_2._id.to_s]
@@ -221,7 +234,7 @@ describe Locomotive::ContentEntry do
221
234
  site = FactoryGirl.build(:site)
222
235
  site.stubs(:accounts).returns([@account_1, @account_2])
223
236
 
224
- @content_entry = build_content_entry(:site => site)
237
+ @content_entry = build_content_entry(site: site)
225
238
  end
226
239
 
227
240
  it 'does not send email notifications if the api is disabled' do
@@ -237,8 +250,8 @@ describe Locomotive::ContentEntry do
237
250
  end
238
251
 
239
252
  it 'sends email notifications when a new instance is created' do
240
- Locomotive::Notifications.expects(:new_content_entry).with(@account_1, @content_entry).returns(mock('mailer', :deliver => true))
241
- Locomotive::Notifications.expects(:new_content_entry).with(@account_2, @content_entry).returns(mock('mailer', :deliver => true))
253
+ Locomotive::Notifications.expects(:new_content_entry).with(@account_1, @content_entry).returns(mock('mailer', deliver: true))
254
+ Locomotive::Notifications.expects(:new_content_entry).with(@account_2, @content_entry).returns(mock('mailer', deliver: true))
242
255
  @content_entry.save
243
256
  end
244
257
 
@@ -260,7 +273,7 @@ describe Locomotive::ContentEntry do
260
273
  end
261
274
 
262
275
  def build_content_entry(options = {})
263
- @content_type.entries.build({ :title => 'Locomotive', :description => 'Lorem ipsum....', :_label_field_name => 'title' }.merge(options))
276
+ @content_type.entries.build({ title: 'Locomotive', description: 'Lorem ipsum....', _label_field_name: 'title' }.merge(options))
264
277
  end
265
278
 
266
279
  def fake_bson_id(id)
@@ -22,4 +22,10 @@ describe 'Locomotive::Middlewares::SeoTrailingSlash' do
22
22
  response.status.should be(301)
23
23
  end
24
24
 
25
+ it 'removes the trailing slash but preserves the query' do
26
+ get '/hello_world/?test=name'
27
+ response.status.should be(301)
28
+ response.location.should == '/hello_world?test=name'
29
+ end
30
+
25
31
  end
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: locomotive_cms
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 2.0.1
5
+ version: 2.0.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Didier Lafforgue
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-04 00:00:00.000000000 Z
12
+ date: 2013-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  type: :runtime
@@ -1104,6 +1104,8 @@ files:
1104
1104
  - lib/locomotive_cms.rb
1105
1105
  - lib/tasks/development.rake
1106
1106
  - lib/tasks/locomotive.rake
1107
+ - mongodb/migrate/20130204072721_make_editable_elements_consistent.rb
1108
+ - mongodb/migrate/20130326201349_rename_entry_to_content_entry.rb
1107
1109
  - public/favicon.ico
1108
1110
  - vendor/assets/fonts/locomotive/fontawesome-webfont.eot
1109
1111
  - vendor/assets/fonts/locomotive/fontawesome-webfont.svg
@@ -1337,7 +1339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
1337
1339
  - !ruby/object:Gem::Version
1338
1340
  segments:
1339
1341
  - 0
1340
- hash: -3303457319512363184
1342
+ hash: -4389909398335603459
1341
1343
  version: '0'
1342
1344
  required_rubygems_version: !ruby/object:Gem::Requirement
1343
1345
  none: false
@@ -1346,7 +1348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1346
1348
  - !ruby/object:Gem::Version
1347
1349
  segments:
1348
1350
  - 0
1349
- hash: -3303457319512363184
1351
+ hash: -4389909398335603459
1350
1352
  version: '0'
1351
1353
  requirements: []
1352
1354
  rubyforge_project: