kiosk 0.0.1 → 0.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.
data/README.rdoc CHANGED
@@ -16,7 +16,23 @@ license.
16
16
 
17
17
  == Configuration
18
18
 
19
- (Documentation is forthcoming.)
19
+ CMS integration requires a WordPress installation that includes the {JSON
20
+ API Plugin}[http://wordpress.org/extend/plugins/json-api/].
21
+
22
+ Once the WP site is up and running, the site endpoint should be specified as
23
+ the +origin+ content server in `config/kiosk.yml` of your Rails application.
24
+ Different configurations may be specified for each Rails environment, along
25
+ with a default.
26
+
27
+ origins:
28
+ default:
29
+ site: 'http://dev.cms.example/site_name'
30
+ production:
31
+ site: 'http://cms.example/site_name'
32
+
33
+ Localization of content resources depends further on the installation of the
34
+ {WPML Multilingual CMS}[http://wpml.org/] (non-free) and {WPML JSON
35
+ API}[http://wordpress.org/extend/plugins/wpml-json-api/] plugins.
20
36
 
21
37
  == Roadmap
22
38
 
data/lib/kiosk.rb CHANGED
@@ -13,6 +13,7 @@ module Kiosk
13
13
  autoload :Cdn, 'kiosk/cdn'
14
14
  autoload :Claim, 'kiosk/claim'
15
15
  autoload :ClaimedNode, 'kiosk/claimed_node'
16
+ autoload :ContentTeaser, 'kiosk/content_teaser'
16
17
  autoload :Controller, 'kiosk/controller'
17
18
  autoload :Document, 'kiosk/document'
18
19
  autoload :Indexer, 'kiosk/indexer'
@@ -20,11 +21,12 @@ module Kiosk
20
21
  autoload :Origin, 'kiosk/origin'
21
22
  autoload :ProspectiveNode, 'kiosk/prospective_node'
22
23
  autoload :Prospector, 'kiosk/prospector'
23
- autoload :Resource, 'kiosk/resource'
24
+ autoload :ResourceController, 'kiosk/resource_controller'
24
25
  autoload :ResourceURI, 'kiosk/resource_uri'
25
26
  autoload :Rewrite, 'kiosk/rewrite'
26
27
  autoload :Rewriter, 'kiosk/rewriter'
27
28
  autoload :Searchable, 'kiosk/searchable'
29
+ autoload :WordPress, 'kiosk/word_press'
28
30
 
29
31
  ##############################################################################
30
32
  # Module methods
@@ -4,6 +4,29 @@ module Kiosk
4
4
  module Cacheable::Resource
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ included do
8
+ def self.inherited(sub)
9
+ super
10
+
11
+ sub.module_exec do
12
+ protected
13
+
14
+ # Returns expireable connection keys set by derived class and all
15
+ # parent classes.
16
+ #
17
+ def self.all_connection_keys_to_expire
18
+ keys = @connection_keys_to_expire || []
19
+
20
+ if superclass.respond_to?(:all_connection_keys_to_expire)
21
+ superclass.all_connection_keys_to_expire | keys
22
+ else
23
+ keys
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
7
30
  module ClassMethods
8
31
  # Specifies the length of time for which a resource should stay cached.
9
32
  # Either a +Fixnum+ (time in seconds) or block can be given. The block
@@ -46,11 +69,9 @@ module Kiosk
46
69
  expire(resource.id)
47
70
  expire_by_slug(resource.slug)
48
71
 
49
- if @connection_keys_to_expire
50
- begin
51
- @connection_keys_to_expire.each { |key| connection.cache_expire_by_pattern(key) }
52
- rescue NotImplementedError
53
- end
72
+ begin
73
+ all_connection_keys_to_expire.each { |key| connection.cache_expire_by_pattern(key) }
74
+ rescue NotImplementedError
54
75
  end
55
76
 
56
77
  notify_observers(:after_expire, resource)
@@ -0,0 +1,41 @@
1
+ require 'active_support/core_ext/string'
2
+
3
+ module Kiosk
4
+ module ContentTeaser
5
+ # Returns a teaser of the content with roughly the given length.
6
+ #
7
+ # The content is parsed as an HTML fragment and only text nodes are
8
+ # considered to have length in this context. As a result, the returned
9
+ # string will not have a length matching the given +horizon+ exactly, but
10
+ # should render as such.
11
+ #
12
+ def teaser(horizon, options = {})
13
+ if doc = content_document
14
+
15
+ # Traverse all text nodes until we reach the limit
16
+ length = 0
17
+
18
+ # Find the boundary node, where the text length crosses the horizon
19
+ node = doc.xpath('descendant::text()').detect do |n|
20
+ (length += n.content.length) >= horizon
21
+ end
22
+
23
+ if node
24
+ # Truncate the content of the boundary text node
25
+ node.content = node.content.truncate(node.content.length - (length - horizon), options)
26
+
27
+ # Remove all following nodes from the document
28
+ # Note that this mark-and-sweep method is due to the catch 22 of
29
+ # relative DOM traversal (next, parent, etc.) and node removal.
30
+ nodes_to_remove = []
31
+ nodes_to_remove << node while node = (node.next || (node.parent && node.parent.next))
32
+ nodes_to_remove.each { |n| n.remove if n.parent }
33
+ end
34
+
35
+ doc.to_html
36
+ else
37
+ ""
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ module Kiosk
2
+ module ResourceController
3
+ def show
4
+ self.resource = resource_model.find_by_slug(params[:id] || params[:slug])
5
+ end
6
+
7
+ # Can be used as an endpoint for the CMS to expire the cache.
8
+ #
9
+ def update
10
+ model = resource_model
11
+
12
+ if model.respond_to?(:expire)
13
+ resource = model.new(params)
14
+
15
+ # Expire for all target locales if the resource is localizable
16
+ if model.respond_to?(:localized_to)
17
+ I18n.available_locales.each do |locale|
18
+ model.localized_to(locale) do
19
+ resource.expire
20
+ end
21
+ end
22
+ else
23
+ resource.expire
24
+ end
25
+ end
26
+
27
+ render :nothing => true
28
+ end
29
+
30
+ private
31
+
32
+ # Resolves the model from the controller name.
33
+ #
34
+ def resource_model
35
+ names = self.class.name.split('::')
36
+ controller = names.pop
37
+ (names.join('::') + '::' + controller.sub('Controller', '').singularize).constantize
38
+ end
39
+
40
+ def resource_name
41
+ resource_model.name.split('::').last.underscore
42
+ end
43
+
44
+ def resource=(resource)
45
+ instance_variable_set("@#{resource_name}".to_sym, resource)
46
+ @resource = resource
47
+ end
48
+ end
49
+ end
@@ -44,10 +44,16 @@ module Kiosk
44
44
  @rewrites.clear
45
45
  end
46
46
 
47
+ # Returns the rewriten document from +rewrite_to_document+ as a string.
48
+ #
49
+ def rewrite(content)
50
+ rewrite_to_document(content).to_html
51
+ end
52
+
47
53
  # Runs on claims on the given content, incorporates all controller
48
54
  # rewrites, and returns the resulting content.
49
55
  #
50
- def rewrite(content)
56
+ def rewrite_to_document(content)
51
57
  document = Document.parse(content)
52
58
 
53
59
  # Claims are grouped by priority. Process them in order.
@@ -66,7 +72,7 @@ module Kiosk
66
72
  end
67
73
  end
68
74
 
69
- document.to_html
75
+ document
70
76
  end
71
77
  end
72
78
  end
data/lib/kiosk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kiosk
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
@@ -0,0 +1,14 @@
1
+ module Kiosk
2
+ module WordPress
3
+ autoload :Attachment, 'kiosk/word_press/attachment'
4
+ autoload :Author, 'kiosk/word_press/author'
5
+ autoload :Category, 'kiosk/word_press/category'
6
+ autoload :Comment, 'kiosk/word_press/comment'
7
+ autoload :Images, 'kiosk/word_press/images'
8
+ autoload :Page, 'kiosk/word_press/page'
9
+ autoload :Post, 'kiosk/word_press/post'
10
+ autoload :Resource, 'kiosk/word_press/resource'
11
+ autoload :Tag, 'kiosk/word_press/tag'
12
+ autoload :Video, 'kiosk/word_press/video'
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Attachment < Resource
4
+ claims_path_content(:selector => 'a, img', :pattern => 'files/\d{4}/\d{2}/:slug')
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Author < Resource
4
+ schema do
5
+ attribute 'name', :string
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Category < Resource
4
+ schema do
5
+ attribute 'title', :string
6
+ end
7
+
8
+ # Retrieves a specific category by its slug. This can't be done directly
9
+ # through the WordPress JSON API, so all categories are traversed instead.
10
+ #
11
+ def self.find_by_slug(slug)
12
+ category = all.detect { |category| category.slug == slug }
13
+ raise ResourceNotFound.new("unknown category `#{slug}'") unless category
14
+ category
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Comment < Resource
4
+ schema do
5
+ attribute 'name', :string
6
+ attribute 'url', :string
7
+ attribute 'date', :string
8
+ attribute 'content', :string
9
+ attribute 'parent', :integer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Images < Resource
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,57 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Page < Resource
4
+ include Searchable::Resource
5
+
6
+ ##############################################################################
7
+ # Content integration
8
+ ##############################################################################
9
+ schema do
10
+ attribute 'title', :string
11
+ attribute 'title_plain', :string
12
+ attribute 'content', :string
13
+ attribute 'excerpt', :string
14
+ end
15
+
16
+ claims_path_content(:selector => 'a',
17
+ :pattern => ':slug',
18
+ :shims => {:slug => /[^\?]+/},
19
+ :priority => :low)
20
+
21
+ ##############################################################################
22
+ # Indexes
23
+ ##############################################################################
24
+ define_index(:content_page) do
25
+ indexes :title, :content
26
+ end
27
+
28
+ ##############################################################################
29
+ # Instance methods
30
+ ##############################################################################
31
+
32
+ # Returns the leading portion of the slug.
33
+ #
34
+ def section
35
+ (s = slug.to_s).empty? ? nil : s.split('/').first
36
+ end
37
+
38
+ # Returns the full page slug. This differs in cases where the page is a
39
+ # child of another in the hierarchy. E.g. where a parent page has slug
40
+ # 'p1' and the child page has slug 'c1', 'p1/c1' would be returned, not
41
+ # simply 'c1'.
42
+ #
43
+ def slug
44
+ begin
45
+ #parse the url
46
+ uri = URI.parse(url)
47
+ #get the route from the original site uri
48
+ route = uri.route_from(Kiosk.origin.site_uri)
49
+ #return just the path
50
+ route.path
51
+ rescue
52
+ attributes[:slug]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,96 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Post < Resource
4
+ include Searchable::Resource
5
+ include ContentTeaser
6
+
7
+ ##############################################################################
8
+ # Content integration
9
+ ##############################################################################
10
+ schema do
11
+ attribute 'title', :string
12
+ attribute 'title_plain', :string
13
+ attribute 'content', :string
14
+ attribute 'excerpt', :string
15
+ attribute 'date', :string
16
+ attribute 'modified', :string
17
+ end
18
+
19
+ claims_path_content(:selector => 'a', :pattern => '\d{4}/\d{2}/\d{2}/:slug')
20
+
21
+ expires_connection_methods('get_category_posts', 'get_tag_posts', 'get_recent_posts')
22
+
23
+ ##############################################################################
24
+ # Indexes
25
+ ##############################################################################
26
+ define_index(:content_post) do
27
+ indexes :title, :content
28
+ end
29
+
30
+ ##############################################################################
31
+ # Class methods
32
+ ##############################################################################
33
+ class << self
34
+ def all
35
+ Category.all.inject([]) { |posts,cat| posts += categorized_as(cat) }.uniq { |p| p.id }
36
+ end
37
+
38
+ # Fetches posts for the given category.
39
+ #
40
+ def categorized_as(category, params = {})
41
+ category = Kiosk::WordPress::Category.find_by_slug(category) if category.is_a?(String)
42
+ find_by_associated(category, {:count => 100000}.merge(params))
43
+ rescue ResourceNotFound
44
+ []
45
+ end
46
+
47
+ # Fetches posts that were created on the given date.
48
+ #
49
+ def created_on(date)
50
+ find(:all, :method => :get_date_posts, :params => [:date => date])
51
+ end
52
+
53
+ # Fetches recently made posts.
54
+ #
55
+ def recent
56
+ find(:all, :method => :get_recent_posts)
57
+ end
58
+
59
+ # Fetches posts with the given tag.
60
+ #
61
+ def tagged_with(tag)
62
+ find_by_associated(tag)
63
+ end
64
+ end
65
+
66
+ ##############################################################################
67
+ # Instance methods
68
+ ##############################################################################
69
+
70
+ # Returns the post categories.
71
+ #
72
+ def categories
73
+ attributes[:categories] || []
74
+ end
75
+
76
+ # Returns the first category of the post.
77
+ #
78
+ def category
79
+ categories.first
80
+ end
81
+
82
+ # The time at which this post was authored.
83
+ #
84
+ def created_at
85
+ attributes[:date].to_time
86
+ end
87
+
88
+ # The time at which this post was modified.
89
+ #
90
+ def modified_at
91
+ attributes[:modified].to_time
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,321 @@
1
+ require 'active_resource'
2
+
3
+ # Proxy for content resources.
4
+ #
5
+ module Kiosk
6
+ module WordPress
7
+ class Resource < ::ActiveResource::Base
8
+ include Cacheable::Resource
9
+ include Localizable::Resource
10
+ include Prospector
11
+
12
+ ##############################################################################
13
+ # ActiveResource config
14
+ ##############################################################################
15
+ self.site = Kiosk.origin.site
16
+ self.format = :json
17
+
18
+ schema do
19
+ attribute 'slug', :string
20
+ end
21
+
22
+ ##############################################################################
23
+ # Caching
24
+ ##############################################################################
25
+ cached_expire_in { |resource| resource['status'] == 'error' ? 30.minutes : 1.day }
26
+
27
+ ##############################################################################
28
+ # Class methods
29
+ ##############################################################################
30
+ class << self
31
+ # Returns all instances of the resource.
32
+ #
33
+ def all
34
+ find(:all)
35
+ end
36
+
37
+ # Reimplements the +ActiveResource+ path constructor to work with the
38
+ # WordPress JSON-API plugin.
39
+ #
40
+ def element_path(id, prefix_options = {}, query_options = nil)
41
+ "#{api_path_to("get_#{element_name}")}#{query_string({:id => id}.merge(query_options || {}))}"
42
+ end
43
+
44
+ def element_path_by_slug(slug, prefix_options = {}, query_options = nil)
45
+ "#{api_path_to("get_#{element_name}")}#{query_string({:slug => slug}.merge(query_options || {}))}"
46
+ end
47
+
48
+ # Adds functionality to the +ActiveResource.find+ method to allow for
49
+ # specifying the WordPress JSON API method that should be used. This
50
+ # simplifies definition of scopes in derived models.
51
+ #
52
+ def find(*arguments)
53
+ scope = arguments.slice!(0)
54
+ options = arguments.slice!(0) || {}
55
+
56
+ if options.key?(:method)
57
+ options[:from] = api_path_to(options[:method])
58
+ options.delete(:method)
59
+ end
60
+
61
+ super(scope, options)
62
+ end
63
+
64
+ # Finds all resources by the given related resource.
65
+ #
66
+ # Example:
67
+ #
68
+ # Post.find_by_associated(category)
69
+ #
70
+ # Is the same as invoking:
71
+ #
72
+ # Post.find(:all, :method => "get_category_posts", :params => {:id => category.id})
73
+ #
74
+ def find_by_associated(resource, params = {})
75
+ find(:all,
76
+ :method => "get_#{resource.class.element_name}_#{element_name.pluralize}",
77
+ :params => params.merge({:id => resource.id}))
78
+ end
79
+
80
+ # Finds the resource by the given slug.
81
+ #
82
+ def find_by_slug(slug)
83
+ find(:one, :method => "get_#{element_name}", :params => {:slug => slug})
84
+ end
85
+
86
+ # Reimplements the +ActiveResource+ path constructor to work with the
87
+ # WordPress JSON-API plugin.
88
+ #
89
+ def collection_path(prefix_options = {}, query_options = nil)
90
+ "#{api_path_to("get_#{element_name}_index")}#{query_string(query_options)}"
91
+ end
92
+
93
+ # Reimplements the +ActiveResource+ method to check for bad responses
94
+ # before instantiating a collection.
95
+ #
96
+ def instantiate_collection(collection, prefix_options = {})
97
+ super(normalize_response(collection, true), prefix_options)
98
+ end
99
+
100
+ # Reimplements the +ActiveResource+ method to check for bad responses
101
+ # before instantiating an object.
102
+ #
103
+ def instantiate_record(record, prefix_options = {})
104
+ super(normalize_response(record), prefix_options)
105
+ end
106
+
107
+ # Executes the given block within a scope where all requests for this
108
+ # content resource are appended with the given parameters.
109
+ #
110
+ # class Post < Resource; end
111
+ #
112
+ # Post.with_parameters(:language => 'en') do
113
+ # english_posts = Post.find(:all)
114
+ # english_pages = Page.find(:all)
115
+ # end
116
+ #
117
+ # Scopes can be nested.
118
+ #
119
+ # Post.with_parameters(:language => 'es') do
120
+ # Post.with_parameters(:recent => true) do
121
+ # recent_spanish_posts = Post.find(:all)
122
+ # end
123
+ # end
124
+ #
125
+ # Scopes are inherited.
126
+ #
127
+ # Resource.with_parameters(:language => 'fr') do
128
+ # french_posts = Post.find(:all)
129
+ # end
130
+ #
131
+ # However, nesting is still respected.
132
+ #
133
+ # Resource.with_parameters(:language => 'fr') do
134
+ # Post.with_parameters(:language => 'en') do
135
+ # english_posts = Post.find(:all)
136
+ # end
137
+ # end
138
+ #
139
+ # Even with this nesting inverted.
140
+ #
141
+ # Post.with_parameters(:language => 'fr') do
142
+ # Resource.with_parameters(:language => 'en') do
143
+ # english_posts = Post.find(:all)
144
+ # end
145
+ # end
146
+ #
147
+ def with_parameters(params = {})
148
+ push_to_query_scope_stack(params)
149
+
150
+ begin
151
+ yield
152
+ ensure
153
+ pop_from_query_scope_stack
154
+ end
155
+ end
156
+
157
+ protected
158
+
159
+ # Returns the path to the given method of the WordPress API.
160
+ #
161
+ def api_path_to(method)
162
+ "#{site.path}api/#{method}/"
163
+ end
164
+
165
+ # Checks the given response for errors and normalizes its structure. A
166
+ # response from the API includes an envelope, which must be checked for
167
+ # the response status ("ok" or "error"). If an error is found, an
168
+ # exception is raised.
169
+ #
170
+ def normalize_response(response, collection = false)
171
+ response = case response['status']
172
+ when 'ok'
173
+ response[collection ? element_name.pluralize : element_name]
174
+ when 'error'
175
+ raise_error(response['error'])
176
+ else
177
+ # This isn't a response envelope. Just let it pass through.
178
+ response
179
+ end
180
+
181
+ camelcase_keys(response)
182
+ end
183
+
184
+ # Reimplements the parent method to include parameters of the current
185
+ # query scope. See +with_parameters+.
186
+ #
187
+ def query_string(options)
188
+ scoped_options = my_query_scope_stack.inject({}) do |scoped_options,(klass,opt_stack)|
189
+ if self.ancestors.include?(klass)
190
+ scoped_options = opt_stack.reduce(scoped_options) do |scoped_options,opts|
191
+ scoped_options.merge(opts)
192
+ end
193
+ else
194
+ scoped_options
195
+ end
196
+ end
197
+
198
+ super(scoped_options.merge(options))
199
+ end
200
+
201
+ # Handles errors returned by the WordPress JSON API.
202
+ #
203
+ def raise_error(error)
204
+ case error
205
+ when 'Not found.'
206
+ raise Kiosk::ResourceNotFound.new(error)
207
+ when /Un?known method '(\w+)'/ # note the possibility of a spelling error
208
+ raise NotImplementedError.new(error)
209
+ else
210
+ raise Kiosk::ResourceError.new(error)
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ mattr_accessor :query_scope_stack
217
+
218
+ # Filters and sorts the query-scope stack so that only scopes relevant
219
+ # to this class are applied and are applied in order from furthest
220
+ # ancestor to nearest.
221
+ #
222
+ def my_query_scope_stack
223
+ if query_scope_stack
224
+ Hash[query_scope_stack.select do |klass,stack|
225
+ self.ancestors.include?(klass)
226
+ end.sort_by do |klass,stack|
227
+ self.ancestors.index(klass) * -1
228
+ end]
229
+ else
230
+ {}
231
+ end
232
+ end
233
+
234
+ def push_to_query_scope_stack(params)
235
+ self.query_scope_stack ||= {}
236
+ self.query_scope_stack[self] ||= []
237
+
238
+ # Append stacks for this class and all descendent classes.
239
+ query_scope_stack.each_key do |klass|
240
+ query_scope_stack[klass].push(params) if klass.ancestors.include?(self)
241
+ end
242
+ end
243
+
244
+ def pop_from_query_scope_stack
245
+ # Pop stacks for this class and all descendent classes.
246
+ query_scope_stack.each_key do |klass|
247
+ query_scope_stack[klass].pop if klass.ancestors.include?(self)
248
+ end
249
+ end
250
+
251
+ # Recursively changes the keys of the given hash (or array of hashes)
252
+ # to camelcase.
253
+ #
254
+ def camelcase_keys(obj)
255
+ case obj
256
+ when Hash
257
+ obj.inject({}) { |hash,(k,v)| hash[k.to_s.underscore] = camelcase_keys(v); hash }
258
+ when Array
259
+ obj.map { |v| camelcase_keys(v) }
260
+ else
261
+ obj
262
+ end
263
+ end
264
+ end
265
+
266
+ ##############################################################################
267
+ # Instance methods
268
+ ##############################################################################
269
+
270
+ # Returns the rewritten resource content. See +raw_content+ for untouched
271
+ # content.
272
+ #
273
+ def content
274
+ @content ||= raw_content && Kiosk.rewriter.rewrite(raw_content)
275
+ end
276
+
277
+ # Returns the rewritten resource content as a +Document+.
278
+ #
279
+ def content_document
280
+ raw_content && Kiosk.rewriter.rewrite_to_document(raw_content)
281
+ end
282
+
283
+ # Returns the rewritten resource excerpt. See +raw_excerpt+ for untouched
284
+ # content.
285
+ #
286
+ def excerpt
287
+ @excerpt ||= raw_excerpt && Kiosk.rewriter.rewrite(raw_excerpt)
288
+ end
289
+
290
+ # Destroying is not supported.
291
+ #
292
+ def destroy
293
+ raise NotImplementedError
294
+ end
295
+
296
+ # Returns the resource content, untouched by the content rewriter.
297
+ #
298
+ def raw_content
299
+ attributes[:content]
300
+ end
301
+
302
+ # Returns the resource excerpt, untouched by the content rewriter.
303
+ #
304
+ def raw_excerpt
305
+ attributes[:excerpt]
306
+ end
307
+
308
+ # Saving is not supported.
309
+ #
310
+ def save
311
+ raise NotImplementedError
312
+ end
313
+
314
+ # Returns the value used in constructing a URL to this object.
315
+ #
316
+ def to_param
317
+ attributes[:slug] || attributes[:id]
318
+ end
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,6 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Tag < Resource
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,36 @@
1
+ module Kiosk
2
+ module WordPress
3
+ class Video < Resource
4
+ schema do
5
+ attribute 'id', :string
6
+ attribute 'slug', :string
7
+ attribute 'classid', :string
8
+ end
9
+
10
+ claims_content(:selector => 'object') do |object|
11
+ if object['id'] && (match = object['id'].match(/^viddler(?:player)?-(\w+)/))
12
+ {:slug => object['id'], :classid => object['classid']}
13
+ end
14
+ end
15
+
16
+ # Returns the video ID, which is either an explicitly set attribute or the
17
+ # trailing string identifier from the slug (following the last "-").
18
+ #
19
+ def id
20
+ attributes[:id] || (attributes[:slug] && attributes[:slug].match(/-(\w+)$/)[1])
21
+ end
22
+
23
+ # Returns the path to the "thumbnail" version of the movie.
24
+ #
25
+ def thumbnail_url
26
+ "http://www.viddler.com/simple/#{id}/"
27
+ end
28
+
29
+ # Returns the path to the "full" version of the movie.
30
+ #
31
+ def url
32
+ "http://www.viddler.com/player/#{id}/"
33
+ end
34
+ end
35
+ end
36
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: kiosk
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.1
5
+ version: 0.0.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Daniel Duvall
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-04-27 00:00:00 Z
13
+ date: 2012-05-02 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: nokogiri
@@ -75,6 +75,7 @@ files:
75
75
  - lib/kiosk/claim/path_claim.rb
76
76
  - lib/kiosk/claim.rb
77
77
  - lib/kiosk/claimed_node.rb
78
+ - lib/kiosk/content_teaser.rb
78
79
  - lib/kiosk/controller.rb
79
80
  - lib/kiosk/document.rb
80
81
  - lib/kiosk/indexer/adapter/base.rb
@@ -87,7 +88,7 @@ files:
87
88
  - lib/kiosk/origin.rb
88
89
  - lib/kiosk/prospective_node.rb
89
90
  - lib/kiosk/prospector.rb
90
- - lib/kiosk/resource.rb
91
+ - lib/kiosk/resource_controller.rb
91
92
  - lib/kiosk/resource_error.rb
92
93
  - lib/kiosk/resource_not_found.rb
93
94
  - lib/kiosk/resource_uri.rb
@@ -100,6 +101,17 @@ files:
100
101
  - lib/kiosk/searchable.rb
101
102
  - lib/kiosk/tasks/kiosk.rake
102
103
  - lib/kiosk/version.rb
104
+ - lib/kiosk/word_press/attachment.rb
105
+ - lib/kiosk/word_press/author.rb
106
+ - lib/kiosk/word_press/category.rb
107
+ - lib/kiosk/word_press/comment.rb
108
+ - lib/kiosk/word_press/images.rb
109
+ - lib/kiosk/word_press/page.rb
110
+ - lib/kiosk/word_press/post.rb
111
+ - lib/kiosk/word_press/resource.rb
112
+ - lib/kiosk/word_press/tag.rb
113
+ - lib/kiosk/word_press/video.rb
114
+ - lib/kiosk/word_press.rb
103
115
  - lib/kiosk.rb
104
116
  - MIT-LICENSE
105
117
  - Rakefile
@@ -1,313 +0,0 @@
1
- require 'active_resource'
2
-
3
- # Proxy for content resources.
4
- #
5
- module Kiosk
6
- class Resource < ::ActiveResource::Base
7
- include Cacheable::Resource
8
- include Localizable::Resource
9
- include Prospector
10
-
11
- ##############################################################################
12
- # ActiveResource config
13
- ##############################################################################
14
- self.site = Kiosk.origin.site
15
- self.format = :json
16
-
17
- schema do
18
- attribute 'slug', :string
19
- end
20
-
21
- ##############################################################################
22
- # Caching
23
- ##############################################################################
24
- cached_expire_in { |resource| resource['status'] == 'error' ? 30.minutes : 1.day }
25
-
26
- ##############################################################################
27
- # Class methods
28
- ##############################################################################
29
- class << self
30
- # Returns all instances of the resource.
31
- #
32
- def all
33
- find(:all)
34
- end
35
-
36
- # Reimplements the +ActiveResource+ path constructor to work with the
37
- # WordPress JSON-API plugin.
38
- #
39
- def element_path(id, prefix_options = {}, query_options = nil)
40
- "#{api_path_to("get_#{element_name}")}#{query_string({:id => id}.merge(query_options || {}))}"
41
- end
42
-
43
- def element_path_by_slug(slug, prefix_options = {}, query_options = nil)
44
- "#{api_path_to("get_#{element_name}")}#{query_string({:slug => slug}.merge(query_options || {}))}"
45
- end
46
-
47
- # Adds functionality to the +ActiveResource.find+ method to allow for
48
- # specifying the WordPress JSON API method that should be used. This
49
- # simplifies definition of scopes in derived models.
50
- #
51
- def find(*arguments)
52
- scope = arguments.slice!(0)
53
- options = arguments.slice!(0) || {}
54
-
55
- if options.key?(:method)
56
- options[:from] = api_path_to(options[:method])
57
- options.delete(:method)
58
- end
59
-
60
- super(scope, options)
61
- end
62
-
63
- # Finds all resources by the given related resource.
64
- #
65
- # Example:
66
- #
67
- # Post.find_by_associated(category)
68
- #
69
- # Is the same as invoking:
70
- #
71
- # Post.find(:all, :method => "get_category_posts", :params => {:id => category.id})
72
- #
73
- def find_by_associated(resource, params = {})
74
- find(:all,
75
- :method => "get_#{resource.class.element_name}_#{element_name.pluralize}",
76
- :params => params.merge({:id => resource.id}))
77
- end
78
-
79
- # Finds the resource by the given slug.
80
- #
81
- def find_by_slug(slug)
82
- find(:one, :method => "get_#{element_name}", :params => {:slug => slug})
83
- end
84
-
85
- # Reimplements the +ActiveResource+ path constructor to work with the
86
- # WordPress JSON-API plugin.
87
- #
88
- def collection_path(prefix_options = {}, query_options = nil)
89
- "#{api_path_to("get_#{element_name}_index")}#{query_string(query_options)}"
90
- end
91
-
92
- # Reimplements the +ActiveResource+ method to check for bad responses
93
- # before instantiating a collection.
94
- #
95
- def instantiate_collection(collection, prefix_options = {})
96
- super(normalize_response(collection, true), prefix_options)
97
- end
98
-
99
- # Reimplements the +ActiveResource+ method to check for bad responses
100
- # before instantiating an object.
101
- #
102
- def instantiate_record(record, prefix_options = {})
103
- super(normalize_response(record), prefix_options)
104
- end
105
-
106
- # Executes the given block within a scope where all requests for this
107
- # content resource are appended with the given parameters.
108
- #
109
- # class Post < Resource; end
110
- #
111
- # Post.with_parameters(:language => 'en') do
112
- # english_posts = Post.find(:all)
113
- # english_pages = Page.find(:all)
114
- # end
115
- #
116
- # Scopes can be nested.
117
- #
118
- # Post.with_parameters(:language => 'es') do
119
- # Post.with_parameters(:recent => true) do
120
- # recent_spanish_posts = Post.find(:all)
121
- # end
122
- # end
123
- #
124
- # Scopes are inherited.
125
- #
126
- # Resource.with_parameters(:language => 'fr') do
127
- # french_posts = Post.find(:all)
128
- # end
129
- #
130
- # However, nesting is still respected.
131
- #
132
- # Resource.with_parameters(:language => 'fr') do
133
- # Post.with_parameters(:language => 'en') do
134
- # english_posts = Post.find(:all)
135
- # end
136
- # end
137
- #
138
- # Even with this nesting inverted.
139
- #
140
- # Post.with_parameters(:language => 'fr') do
141
- # Resource.with_parameters(:language => 'en') do
142
- # english_posts = Post.find(:all)
143
- # end
144
- # end
145
- #
146
- def with_parameters(params = {})
147
- push_to_query_scope_stack(params)
148
-
149
- begin
150
- yield
151
- ensure
152
- pop_from_query_scope_stack
153
- end
154
- end
155
-
156
- protected
157
-
158
- # Returns the path to the given method of the WordPress API.
159
- #
160
- def api_path_to(method)
161
- "#{site.path}api/#{method}/"
162
- end
163
-
164
- # Checks the given response for errors and normalizes its structure. A
165
- # response from the API includes an envelope, which must be checked for
166
- # the response status ("ok" or "error"). If an error is found, an
167
- # exception is raised.
168
- #
169
- def normalize_response(response, collection = false)
170
- response = case response['status']
171
- when 'ok'
172
- response[collection ? element_name.pluralize : element_name]
173
- when 'error'
174
- raise_error(response['error'])
175
- else
176
- # This isn't a response envelope. Just let it pass through.
177
- response
178
- end
179
-
180
- camelcase_keys(response)
181
- end
182
-
183
- # Reimplements the parent method to include parameters of the current
184
- # query scope. See +with_parameters+.
185
- #
186
- def query_string(options)
187
- scoped_options = my_query_scope_stack.inject({}) do |scoped_options,(klass,opt_stack)|
188
- if self.ancestors.include?(klass)
189
- scoped_options = opt_stack.reduce(scoped_options) do |scoped_options,opts|
190
- scoped_options.merge(opts)
191
- end
192
- else
193
- scoped_options
194
- end
195
- end
196
-
197
- super(scoped_options.merge(options))
198
- end
199
-
200
- # Handles errors returned by the WordPress JSON API.
201
- #
202
- def raise_error(error)
203
- case error
204
- when 'Not found.'
205
- raise Kiosk::ResourceNotFound.new(error)
206
- when /Un?known method '(\w+)'/ # note the possibility of a spelling error
207
- raise NotImplementedError.new(error)
208
- else
209
- raise Kiosk::ResourceError.new(error)
210
- end
211
- end
212
-
213
- private
214
-
215
- mattr_accessor :query_scope_stack
216
-
217
- # Filters and sorts the query-scope stack so that only scopes relevant
218
- # to this class are applied and are applied in order from furthest
219
- # ancestor to nearest.
220
- #
221
- def my_query_scope_stack
222
- if query_scope_stack
223
- Hash[query_scope_stack.select do |klass,stack|
224
- self.ancestors.include?(klass)
225
- end.sort_by do |klass,stack|
226
- self.ancestors.index(klass) * -1
227
- end]
228
- else
229
- {}
230
- end
231
- end
232
-
233
- def push_to_query_scope_stack(params)
234
- self.query_scope_stack ||= {}
235
- self.query_scope_stack[self] ||= []
236
-
237
- # Append stacks for this class and all descendent classes.
238
- query_scope_stack.each_key do |klass|
239
- query_scope_stack[klass].push(params) if klass.ancestors.include?(self)
240
- end
241
- end
242
-
243
- def pop_from_query_scope_stack
244
- # Pop stacks for this class and all descendent classes.
245
- query_scope_stack.each_key do |klass|
246
- query_scope_stack[klass].pop if klass.ancestors.include?(self)
247
- end
248
- end
249
-
250
- # Recursively changes the keys of the given hash (or array of hashes)
251
- # to camelcase.
252
- #
253
- def camelcase_keys(obj)
254
- case obj
255
- when Hash
256
- obj.inject({}) { |hash,(k,v)| hash[k.to_s.underscore] = camelcase_keys(v); hash }
257
- when Array
258
- obj.map { |v| camelcase_keys(v) }
259
- else
260
- obj
261
- end
262
- end
263
- end
264
-
265
- ##############################################################################
266
- # Instance methods
267
- ##############################################################################
268
-
269
- # Returns the rewritten resource content. See +raw_content+ for untouched
270
- # content.
271
- #
272
- def content
273
- @content ||= raw_content && Kiosk.rewriter.rewrite(raw_content)
274
- end
275
-
276
- # Returns the rewritten resource excerpt. See +raw_excerpt+ for untouched
277
- # content.
278
- #
279
- def excerpt
280
- @excerpt ||= raw_excerpt && Kiosk.rewriter.rewrite(raw_excerpt)
281
- end
282
-
283
- # Destroying is not supported.
284
- #
285
- def destroy
286
- raise NotImplementedError
287
- end
288
-
289
- # Returns the resource content, untouched by the content rewriter.
290
- #
291
- def raw_content
292
- attributes[:content]
293
- end
294
-
295
- # Returns the resource excerpt, untouched by the content rewriter.
296
- #
297
- def raw_excerpt
298
- attributes[:excerpt]
299
- end
300
-
301
- # Saving is not supported.
302
- #
303
- def save
304
- raise NotImplementedError
305
- end
306
-
307
- # Returns the value used in constructing a URL to this object.
308
- #
309
- def to_param
310
- attributes[:slug] || attributes[:id]
311
- end
312
- end
313
- end