kiosk 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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