spontaneous 0.2.0.beta9 → 0.2.0.beta10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/LICENSE +18 -17
  4. data/Rakefile +1 -1
  5. data/application/css/core.css.scss +1 -1
  6. data/application/css/dialogue.css.scss +8 -20
  7. data/application/js/preview.js +28 -7
  8. data/application/js/publish.js +15 -4
  9. data/application/js/top_bar.js +0 -16
  10. data/application/js/views/piece_view.js +1 -1
  11. data/lib/spontaneous/asset/environment.rb +16 -1
  12. data/lib/spontaneous/box.rb +68 -0
  13. data/lib/spontaneous/capistrano/deploy.rb +7 -4
  14. data/lib/spontaneous/capistrano/sync.rb +2 -2
  15. data/lib/spontaneous/cli/init.rb +70 -19
  16. data/lib/spontaneous/cli/init/db.rb +34 -55
  17. data/lib/spontaneous/cli/init/mysql.rb +5 -5
  18. data/lib/spontaneous/cli/init/postgresql.rb +8 -9
  19. data/lib/spontaneous/cli/init/sqlite.rb +1 -2
  20. data/lib/spontaneous/cli/migrate.rb +0 -1
  21. data/lib/spontaneous/cli/site.rb +4 -0
  22. data/lib/spontaneous/collections/entry_set.rb +11 -0
  23. data/lib/spontaneous/data_mapper/content_model.rb +2 -0
  24. data/lib/spontaneous/data_mapper/content_model/serialization.rb +2 -2
  25. data/lib/spontaneous/extensions/array.rb +12 -2
  26. data/lib/spontaneous/field/base.rb +10 -0
  27. data/lib/spontaneous/field/file.rb +32 -2
  28. data/lib/spontaneous/field/image.rb +24 -2
  29. data/lib/spontaneous/field/select.rb +8 -0
  30. data/lib/spontaneous/field/webvideo.rb +8 -0
  31. data/lib/spontaneous/generators/site/config/initializers/fields.rb +55 -0
  32. data/lib/spontaneous/json.rb +3 -2
  33. data/lib/spontaneous/logger.rb +2 -2
  34. data/lib/spontaneous/media/file.rb +3 -3
  35. data/lib/spontaneous/media/image/attributes.rb +72 -6
  36. data/lib/spontaneous/media/image/renderable.rb +53 -20
  37. data/lib/spontaneous/media/store.rb +3 -3
  38. data/lib/spontaneous/media/store/backend.rb +16 -0
  39. data/lib/spontaneous/media/store/cloud.rb +52 -12
  40. data/lib/spontaneous/media/store/local.rb +6 -3
  41. data/lib/spontaneous/model.rb +3 -0
  42. data/lib/spontaneous/model/core/entries.rb +34 -13
  43. data/lib/spontaneous/model/core/entry.rb +3 -1
  44. data/lib/spontaneous/model/page/controllers.rb +1 -2
  45. data/lib/spontaneous/model/page/paths.rb +18 -7
  46. data/lib/spontaneous/output/context.rb +0 -8
  47. data/lib/spontaneous/output/template/renderer.rb +2 -0
  48. data/lib/spontaneous/plugins/application/state.rb +0 -4
  49. data/lib/spontaneous/prototypes/field_prototype.rb +4 -0
  50. data/lib/spontaneous/publishing/immediate.rb +0 -5
  51. data/lib/spontaneous/publishing/progress.rb +2 -2
  52. data/lib/spontaneous/publishing/rerender.rb +1 -4
  53. data/lib/spontaneous/publishing/simultaneous.rb +19 -17
  54. data/lib/spontaneous/publishing/steps.rb +12 -3
  55. data/lib/spontaneous/rack.rb +2 -0
  56. data/lib/spontaneous/rack/asset_server.rb +5 -2
  57. data/lib/spontaneous/rack/back.rb +9 -1
  58. data/lib/spontaneous/rack/back/base.rb +1 -0
  59. data/lib/spontaneous/rack/back/changes.rb +5 -0
  60. data/lib/spontaneous/rack/back/preview.rb +4 -4
  61. data/lib/spontaneous/rack/back/private.rb +11 -0
  62. data/lib/spontaneous/rack/middleware/scope.rb +16 -4
  63. data/lib/spontaneous/rack/page_controller.rb +2 -2
  64. data/lib/spontaneous/rack/public.rb +52 -4
  65. data/lib/spontaneous/sequel.rb +10 -13
  66. data/lib/spontaneous/site.rb +28 -8
  67. data/lib/spontaneous/site/publishing.rb +1 -1
  68. data/lib/spontaneous/site/storage.rb +7 -4
  69. data/lib/spontaneous/tasks/environment.rake +3 -0
  70. data/lib/spontaneous/utils/database/postgres_dumper.rb +23 -2
  71. data/lib/spontaneous/version.rb +1 -1
  72. data/spontaneous.gemspec +7 -12
  73. data/test/fixtures/assets/public1/css/data.css.scss +1 -1
  74. data/test/functional/test_application.rb +15 -0
  75. data/test/functional/test_cli.rb +109 -3
  76. data/test/functional/test_front.rb +108 -10
  77. data/test/test_helper.rb +3 -3
  78. data/test/unit/fields/test_boolean_fields.rb +80 -0
  79. data/test/unit/fields/test_date_fields.rb +47 -0
  80. data/test/unit/fields/test_file_field.rb +210 -0
  81. data/test/unit/{test_images.rb → fields/test_image_fields.rb} +133 -15
  82. data/test/unit/fields/test_location_fields.rb +41 -0
  83. data/test/unit/fields/test_option_fields.rb +61 -0
  84. data/test/unit/fields/test_tag_list_fields.rb +45 -0
  85. data/test/unit/fields/test_text_fields.rb +124 -0
  86. data/test/unit/fields/test_web_video_fields.rb +198 -0
  87. data/test/unit/test_assets.rb +22 -22
  88. data/test/unit/test_boxes.rb +34 -13
  89. data/test/unit/test_changesets.rb +1 -0
  90. data/test/unit/test_extensions.rb +17 -0
  91. data/test/unit/test_fields.rb +20 -643
  92. data/test/unit/test_media.rb +9 -9
  93. data/test/unit/test_page.rb +47 -0
  94. data/test/unit/test_publishing_pipeline.rb +2 -2
  95. data/test/unit/test_serialisation.rb +37 -0
  96. data/test/unit/test_storage.rb +42 -3
  97. metadata +37 -17
@@ -22,9 +22,11 @@ module Spontaneous::Publishing
22
22
  end
23
23
  end
24
24
 
25
- def self.rerender
26
- new do
27
- run :render_revision
25
+ def self.rerender(publishing_steps)
26
+ new([], publishing_steps.progress) do
27
+ RERENDER_STEPS.each do |step|
28
+ run step
29
+ end
28
30
  end
29
31
  end
30
32
 
@@ -140,6 +142,13 @@ module Spontaneous::Publishing
140
142
  :archive_old_revisions
141
143
  ].freeze
142
144
 
145
+ RERENDER_STEPS = [
146
+ :render_revision,
147
+ :copy_assets,
148
+ :copy_static_files,
149
+ :generate_rackup_file
150
+ ].freeze
151
+
143
152
  CORE_PROGRESS = [:browser, :stdout].freeze
144
153
  end
145
154
  end
@@ -15,6 +15,8 @@ module Spontaneous
15
15
  HTTP_LAST_MODIFIED = "Last-Modified".freeze
16
16
  HTTP_NO_CACHE = "max-age=0, must-revalidate, no-cache, no-store".freeze
17
17
 
18
+ SLASH = Spontaneous::Constants::SLASH
19
+
18
20
  NAMESPACE = "/@spontaneous".freeze
19
21
  AUTH_COOKIE = "spontaneous_api_key".freeze
20
22
  SESSION_LIFETIME = 1.year
@@ -3,6 +3,8 @@ module Spontaneous::Rack
3
3
  # header. This wrapper class proxies all requests to a Sprockets enviroment
4
4
  # and adds in a charset setting to the content-type header of all responses
5
5
  class AssetServer
6
+ CONTENT_TYPE = "Content-Type".freeze
7
+
6
8
  def initialize(environment, charset = "UTF-8")
7
9
  @environment, @charset = environment, charset
8
10
  end
@@ -12,8 +14,9 @@ module Spontaneous::Rack
12
14
  end
13
15
 
14
16
  def force_encoding(status, headers, body)
15
- content_type = headers["Content-Type"]
16
- headers.update("Content-Type" => "#{content_type}; charset=#{@charset}")
17
+ if (content_type = headers[CONTENT_TYPE])
18
+ headers.update(CONTENT_TYPE => "#{content_type}; charset=#{@charset}")
19
+ end
17
20
  [status, headers, body]
18
21
  end
19
22
  end
@@ -18,6 +18,7 @@ module Spontaneous
18
18
  autoload :Map, 'spontaneous/rack/back/map'
19
19
  autoload :Page, 'spontaneous/rack/back/page'
20
20
  autoload :Preview, 'spontaneous/rack/back/preview'
21
+ autoload :Private, 'spontaneous/rack/back/private'
21
22
  autoload :Schema, 'spontaneous/rack/back/schema'
22
23
  autoload :Site, 'spontaneous/rack/back/site'
23
24
  autoload :SiteAssets, 'spontaneous/rack/back/site_assets'
@@ -48,6 +49,7 @@ module Spontaneous
48
49
 
49
50
  def self.editing_app(site)
50
51
  ::Rack::Builder.app do
52
+ use ::Rack::ShowExceptions if site.development?
51
53
  use Scope::Edit, site
52
54
  use Transaction, site
53
55
  use ApplicationAssets
@@ -62,6 +64,10 @@ module Spontaneous
62
64
  map("/schema") { run Schema }
63
65
  use Reloader, site
64
66
  use Index
67
+ map("/private") {
68
+ use Scope::Preview, site
69
+ run Private
70
+ }
65
71
  use CSRF::Verification # Everything after this middleware requires a valid CSRF token
66
72
  Back.api_handlers.each do |path, app|
67
73
  map(path) { run app }
@@ -73,6 +79,7 @@ module Spontaneous
73
79
 
74
80
  def self.preview_app(site)
75
81
  ::Rack::Builder.app do
82
+ use ::Rack::ShowExceptions if site.development?
76
83
  use ::Rack::Lint if Spontaneous.development?
77
84
  use Scope::Preview, site
78
85
  use Transaction, site
@@ -84,7 +91,6 @@ module Spontaneous
84
91
  # the preview site.
85
92
  use Authenticate::Preview
86
93
  use CSRF::Header
87
- map("/assets") { run SiteAssets.new }
88
94
  use Spontaneous::Rack::Static, root: Spontaneous.root / "public", urls: %w[/], try: ['.html', 'index.html', '/index.html']
89
95
  use Reloader, site
90
96
  # inject the front controllers into the preview so that this is a
@@ -128,6 +134,8 @@ module Spontaneous
128
134
  end
129
135
  end if site
130
136
 
137
+ map("/assets") { run SiteAssets.new }
138
+
131
139
  map "/media" do
132
140
  use ::Rack::Lint
133
141
  run Spontaneous::Rack::CacheableFile.new(Spontaneous.media_dir)
@@ -4,6 +4,7 @@ module Spontaneous::Rack
4
4
  helpers Helpers
5
5
 
6
6
  set :views, Proc.new { Spontaneous.application_dir + '/views' }
7
+ set :show_exceptions, false
7
8
 
8
9
  def content_model
9
10
  site.model
@@ -15,5 +15,10 @@ module Spontaneous::Rack::Back
15
15
  site.publish_pages(pages)
16
16
  json({})
17
17
  end
18
+
19
+ post '/rerender' do
20
+ site.rerender
21
+ json({})
22
+ end
18
23
  end
19
24
  end
@@ -2,12 +2,12 @@ module Spontaneous::Rack::Back
2
2
  class Preview < Base
3
3
  include Spontaneous::Rack::Public
4
4
 
5
+ set :show_exceptions, proc { Spontaneous.development? || Spontaneous.test? }
6
+
5
7
  # In preview mode we want to find pages even if they're
6
8
  # invisible.
7
- def find_page_by_path(path)
8
- site.model.scope do
9
- site.by_path(path)
10
- end
9
+ def with_scope(&block)
10
+ site.model.scope(&block)
11
11
  end
12
12
 
13
13
  # Redirect to the edit UI if a preview page is being accessed directly
@@ -0,0 +1,11 @@
1
+ module Spontaneous::Rack::Back
2
+ class Private < Base
3
+ include Spontaneous::Rack::Public
4
+
5
+ get '/:id.?:format?' do
6
+ content_for_request { |page|
7
+ _render_page_with_output(page, "html", {})
8
+ }
9
+ end
10
+ end
11
+ end
@@ -41,7 +41,7 @@ module Spontaneous::Rack::Middleware
41
41
  end
42
42
 
43
43
  POWERED_BY = {
44
- "X-Powered-By" => "Spontaneous CMS v#{Spontaneous::VERSION}"
44
+ "X-Powered-By" => "Spontaneous v#{Spontaneous::VERSION} <http://spontaneous.io>"
45
45
  }
46
46
 
47
47
  class Front < Base
@@ -49,18 +49,30 @@ module Spontaneous::Rack::Middleware
49
49
 
50
50
  def initialize(app, site, options = {})
51
51
  super
52
- @renderer = Spontaneous::Output.published_renderer(@site)
53
52
  end
54
53
 
55
54
  def call!(env)
56
55
  status = headers = body = nil
57
- env[RENDERER] = @renderer
58
- env[REVISION] = @site.published_revision
56
+ env[RENDERER] = renderer
57
+ env[REVISION] = revision = @site.published_revision
59
58
  @site.model.with_published(@site) do
60
59
  status, headers, body = @app.call(env)
61
60
  end
62
61
  [status, headers.merge(POWERED_BY), body]
63
62
  end
63
+
64
+ def renderer
65
+ return renderer_for_revision if development?
66
+ @renderer ||= renderer_for_revision
67
+ end
68
+
69
+ def renderer_for_revision
70
+ Spontaneous::Output.published_renderer(@site)
71
+ end
72
+
73
+ def development?
74
+ Spontaneous.development?
75
+ end
64
76
  end
65
77
  end
66
78
  end
@@ -6,7 +6,7 @@ module Spontaneous::Rack
6
6
  class PageController < Sinatra::Base
7
7
  class << self
8
8
  # We wrap Sinatra's route methods in order to do two things:
9
- # 1. To provide a path of '/' when none is given and
9
+ # 1. To provide a path of '*' when none is given and
10
10
  # 2. To register the presence of a handler for each method in order to
11
11
  # correctly respond to the #dynamic? test
12
12
  def get(*args, &bk) __dynamic!(:get, super(*__route_args(args), &bk)) end
@@ -28,7 +28,7 @@ module Spontaneous::Rack
28
28
 
29
29
  def __route_args(args)
30
30
  opts = args.extract_options!
31
- path = String === args.first ? args.first : '/'
31
+ path = (String === args.first) ? args.first : S::Constants::SLASH
32
32
  [path, opts]
33
33
  end
34
34
 
@@ -87,12 +87,52 @@ module Spontaneous
87
87
  end
88
88
 
89
89
  def find_page!(path)
90
+ @controller_path = SLASH
90
91
  @path, @output, @action = parse_path(path)
91
92
  @page = find_page_by_path(@path)
92
93
  end
93
94
 
94
95
  def find_page_by_path(path)
95
- site.by_path(path)
96
+ with_scope { site.by_path(path) || find_page_with_wildcards(path) }
97
+ end
98
+
99
+ # if we get to here it's because the path hasn't been found. This will get called for
100
+ # every request where the request doesn’t resolve to a path found in the db
101
+ # and will always try the site homepage as a last resort. So if you need many dynamic
102
+ # routes to resolve to a single page, e.g. for a single page app, then you just
103
+ # need to accept all those routes in a controller on the class of the site’s homepage
104
+ # and render your SPA template from that, e.g.
105
+ #
106
+ # class Homepage < Page
107
+ # controller do
108
+ # get '/app*' do
109
+ # render
110
+ # end
111
+ # end
112
+ # end
113
+ #
114
+ def find_page_with_wildcards(path)
115
+ parts = path.split('/')
116
+ length = parts.length - 2
117
+ range = (1..length).to_a.reverse
118
+
119
+ # make sure we go all the way back to the site homepage
120
+ try = range.map { |l| parts[0..l].join(SLASH) }.push(SLASH)
121
+ candidate = site.model::Page.where(path: try).order(Sequel.desc(:depth)).first
122
+ return nil if candidate.nil? || !candidate.dynamic?(request.request_method)
123
+
124
+ # don't pass the full path of the request to the controller, just
125
+ # the bit after the candidate page’s path.
126
+ cpath = candidate.path
127
+ @controller_path = path.slice(cpath.length, path.length - cpath.length)
128
+
129
+ # special handling of root, as always so that a controller on the root page that
130
+ # matches '/', e.g. `get '/'` is passed a path that starts with '/'
131
+ if cpath == SLASH
132
+ @controller_path.insert(0, SLASH)
133
+ end
134
+
135
+ candidate
96
136
  end
97
137
 
98
138
  def output(name)
@@ -106,7 +146,7 @@ module Spontaneous
106
146
  def render_get
107
147
  return call_action! if @action
108
148
  if page.dynamic?(request.request_method)
109
- invoke_action { page.process_root_action(site, env.dup, @output) }
149
+ invoke_action { page.process_root_action(site, env_for_action, @output) }
110
150
  else
111
151
  render_page_with_output
112
152
  end
@@ -119,11 +159,15 @@ module Spontaneous
119
159
 
120
160
  return call_action! if @action
121
161
 
122
- invoke_action { page.process_root_action(site, env.dup, @output) }
162
+ invoke_action { page.process_root_action(site, env_for_action, @output) }
123
163
  end
124
164
 
125
165
  def call_action!
126
- invoke_action { @page.process_action(site, action, env.dup, @output) }
166
+ invoke_action { @page.process_action(site, action, env_for_action, @output) }
167
+ end
168
+
169
+ def env_for_action
170
+ env.merge(S::Constants::PATH_INFO => @controller_path)
127
171
  end
128
172
 
129
173
  def invoke_action
@@ -185,6 +229,10 @@ module Spontaneous
185
229
  def not_found!
186
230
  404
187
231
  end
232
+
233
+ def with_scope
234
+ yield
235
+ end
188
236
  end
189
237
  end
190
238
  end
@@ -2,16 +2,13 @@ require "sequel"
2
2
 
3
3
  Sequel.extension :inflector
4
4
 
5
- require 'sequel/plugins/serialization'
6
-
7
- Sequel::Plugins::Serialization.register_format(
8
- :ojson,
9
- lambda { |v| Yajl::Encoder.new.encode(v) },
10
- lambda { |v| Yajl::Parser.new(:symbolize_keys => true).parse(v) }
11
- )
12
- # Sequel::Plugins::Serialization.register_format(
13
- # :ojson,
14
- # lambda { |v| Oj.dump(v) },
15
- # lambda { |v| Oj.load(v, symbol_keys: true) }
16
- # )
17
-
5
+ # See http://sequel.jeremyevans.net/rdoc/classes/Sequel/Timezones.html
6
+ # UTC is more performant than :local (or 'nil' which just fallsback to :local)
7
+ # A basic profiling run gives a 2 x performance improvement of :utc over :local
8
+ # With ~240 rows, timing ::Content.all gives:
9
+ #
10
+ # :utc ~0.04s
11
+ # :local ~0.08s
12
+ #
13
+ # DB timestamps are only shown in the editing UI & could be localized there per-user
14
+ Sequel.default_timezone = :utc
@@ -84,19 +84,31 @@ module Spontaneous
84
84
 
85
85
 
86
86
  def connect_to_database!
87
- db = Sequel.connect(db_settings)
88
- db.logger = logger if config.log_queries
89
- # Improve performance for postgres
90
- db.optimize_model_load = true if db.respond_to?(:optimize_model_load)
91
- self.database = db
87
+ self.database = database_instance(db_settings).tap do |db|
88
+ db.logger = logger if config.log_queries
89
+ # Improve performance for postgres
90
+ db.optimize_model_load = true if db.respond_to?(:optimize_model_load)
91
+ end
92
+ end
93
+
94
+ def database_instance(opts)
95
+ Sequel.connect(opts)
92
96
  end
93
97
 
94
98
  def db_settings
95
- self.config.db = db_config[environment]
99
+ self.config.db ||= db_connection_options(environment)
96
100
  end
97
101
 
98
- def db_config
99
- @db_config ||= YAML.load_file(File.join(paths.expanded(:config).first, "database.yml"))
102
+ def db_connection_options(env)
103
+ (db_config_env || db_config_file[env])
104
+ end
105
+
106
+ def db_config_env
107
+ ENV['DATABASE_URL']
108
+ end
109
+
110
+ def db_config_file
111
+ YAML.load_file(File.join(paths.expanded(:config).first, "database.yml"))
100
112
  end
101
113
 
102
114
  def transaction(&block)
@@ -161,5 +173,13 @@ module Spontaneous
161
173
  return cache_root if path.empty?
162
174
  File.join(cache_root, *path)
163
175
  end
176
+
177
+ def development?
178
+ Spontaneous.development?
179
+ end
180
+
181
+ def inspect
182
+ %[#<Site @root="#@root" @schema=#{@schema.inspect} @paths=#{@paths.inspect} @environment=#{@environment.inspect} @mode=#{@mode.inspect}>]
183
+ end
164
184
  end
165
185
  end
@@ -43,7 +43,7 @@ class Spontaneous::Site
43
43
  end
44
44
 
45
45
  def rerender_steps
46
- Spontaneous::Publishing::Steps.rerender
46
+ Spontaneous::Publishing::Steps.rerender(publish_steps)
47
47
  end
48
48
 
49
49
  def publishing_method
@@ -20,8 +20,11 @@ class Spontaneous::Site
20
20
  module Storage
21
21
  extend Spontaneous::Concern
22
22
 
23
- def storage(mimetype = nil)
24
- storage_for_mimetype(mimetype)
23
+ DEFAULT_STORAGE_NAME = 'default'.freeze
24
+
25
+ def storage(name = nil)
26
+ return storage_backends.first if name.nil?
27
+ storage_backends.detect { |storage| storage.name == name } || default_storage
25
28
  end
26
29
 
27
30
  def storage_for_mimetype(mimetype)
@@ -40,14 +43,14 @@ class Spontaneous::Site
40
43
  storage_backends = []
41
44
  storage_settings = config[:storage] || []
42
45
  storage_settings.each do |name, config|
43
- backend = Spontaneous::Media::Store.create(config)
46
+ backend = Spontaneous::Media::Store.create(name.to_s, config)
44
47
  storage_backends << backend
45
48
  end
46
49
  storage_backends << default_storage
47
50
  end
48
51
 
49
52
  def default_storage
50
- @default_storage ||= Spontaneous::Media::Store::Local.new(Spontaneous.media_dir, '/media', accepts=nil)
53
+ @default_storage ||= Spontaneous::Media::Store::Local.new(DEFAULT_STORAGE_NAME, Spontaneous.media_dir, '/media', accepts=nil)
51
54
  end
52
55
 
53
56
  def file(owner, filename, headers = {})
@@ -0,0 +1,3 @@
1
+
2
+ task :environment do
3
+ end
@@ -18,7 +18,8 @@ module Spontaneous
18
18
  options = [
19
19
  "psql",
20
20
  "--quiet",
21
- option(:password),
21
+ option(:host),
22
+ option(:port),
22
23
  option(:username),
23
24
  database_name
24
25
  ]
@@ -42,7 +43,8 @@ module Spontaneous
42
43
  "--clean",
43
44
  "--no-owner",
44
45
  "--no-privileges",
45
- option(:password),
46
+ option(:host),
47
+ option(:port),
46
48
  option(:username),
47
49
  option(:encoding),
48
50
  option(:exclude_table),
@@ -69,6 +71,14 @@ module Spontaneous
69
71
  @database.opts[:password]
70
72
  end
71
73
 
74
+ def host
75
+ @database.opts[:host]
76
+ end
77
+
78
+ def port
79
+ @database.opts[:port]
80
+ end
81
+
72
82
  def encoding
73
83
  "UTF8"
74
84
  end
@@ -76,6 +86,17 @@ module Spontaneous
76
86
  def exclude_table
77
87
  revision_archive_table
78
88
  end
89
+
90
+ def system(cmd)
91
+ begin
92
+ if (pass = password)
93
+ ENV['PGPASSWORD'] = pass
94
+ end
95
+ super
96
+ ensure
97
+ ENV.delete('PGPASSWORD')
98
+ end
99
+ end
79
100
  end
80
101
  end
81
102
  end