myobie-turbine-core 0.1.0
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 +6 -0
- data/Rakefile +13 -0
- data/VERSION.yml +4 -0
- data/lib/ext.rb +63 -0
- data/lib/importers/json_importer.rb +14 -0
- data/lib/importers/text_importer.rb +113 -0
- data/lib/post_type.rb +362 -0
- data/lib/types/article.rb +10 -0
- data/lib/types/audio.rb +10 -0
- data/lib/types/chat.rb +29 -0
- data/lib/types/link.rb +26 -0
- data/lib/types/photo.rb +9 -0
- data/lib/types/quote.rb +27 -0
- data/lib/types/review.rb +14 -0
- data/lib/types/video.rb +10 -0
- data/spec/post_type_spec.rb +174 -0
- data/spec/post_types/chat_spec.rb +25 -0
- data/spec/post_types/link_spec.rb +25 -0
- data/spec/post_types/quote_spec.rb +35 -0
- data/spec/post_types/review_spec.rb +30 -0
- data/spec/spec.opts +0 -0
- data/spec/spec_helper.rb +60 -0
- metadata +79 -0
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "turbine-core"
|
5
|
+
gemspec.summary = "TODO"
|
6
|
+
gemspec.email = "nathan@myobie.com"
|
7
|
+
gemspec.homepage = "http://github.com/myobie/turbine-core"
|
8
|
+
gemspec.description = "TODO"
|
9
|
+
gemspec.authors = ["Nathan Herald"]
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
13
|
+
end
|
data/VERSION.yml
ADDED
data/lib/ext.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
class String
|
2
|
+
def slugify
|
3
|
+
self.dup.slugify!
|
4
|
+
end
|
5
|
+
|
6
|
+
def slugify!
|
7
|
+
self.gsub!(/[^\x00-\x7F]+/, '') # Remove non-ASCII (e.g. diacritics).
|
8
|
+
self.gsub!(/[^a-z0-9\-_\+]+/i, '-') # Turn non-slug chars into the separator.
|
9
|
+
self.gsub!(/-{2,}/, '-') # No more than one of the separator in a row.
|
10
|
+
self.gsub!(/^-|-$/, '') # Remove leading/trailing separator.
|
11
|
+
self.downcase!
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def make_attr
|
16
|
+
self.downcase.to_sym
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Symbol
|
21
|
+
def html
|
22
|
+
(self.to_s + '_html').to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_attr
|
26
|
+
self.to_s.downcase.to_sym
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Array
|
31
|
+
def make_attrs
|
32
|
+
self.collect! { |a| a.to_s.downcase.to_sym }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
### class instance vars that inherit down to children
|
37
|
+
module ClassLevelInheritableAttributes
|
38
|
+
|
39
|
+
PRIMITIVES = [NilClass, TrueClass, FalseClass, Fixnum, Float]
|
40
|
+
|
41
|
+
def cattr_inheritable(*args)
|
42
|
+
@cattr_inheritable_attrs ||= [:cattr_inheritable_attrs]
|
43
|
+
@cattr_inheritable_attrs += args
|
44
|
+
args.each do |arg|
|
45
|
+
class_eval %(
|
46
|
+
class << self; attr_accessor :#{arg}; end
|
47
|
+
)
|
48
|
+
end
|
49
|
+
@cattr_inheritable_attrs
|
50
|
+
end
|
51
|
+
|
52
|
+
def inherited(subclass)
|
53
|
+
@all_children ||= []
|
54
|
+
@cattr_inheritable_attrs.each do |inheritable_attribute|
|
55
|
+
instance_var = "@#{inheritable_attribute}"
|
56
|
+
current_value = instance_variable_get(instance_var)
|
57
|
+
current_value = current_value.dup unless PRIMITIVES.include?(current_value.class)
|
58
|
+
subclass.instance_variable_set(instance_var, current_value)
|
59
|
+
end
|
60
|
+
@all_children << subclass
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class TextImporter
|
2
|
+
|
3
|
+
attr_accessor :remainder, :result, :type
|
4
|
+
|
5
|
+
def initialize(type)
|
6
|
+
@type = type
|
7
|
+
@result = []
|
8
|
+
@remainder = ''
|
9
|
+
@primary = nil
|
10
|
+
@heading = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def import(text)
|
14
|
+
pull_pairs(text)
|
15
|
+
eval_primary_field
|
16
|
+
|
17
|
+
@result
|
18
|
+
end
|
19
|
+
|
20
|
+
def pull_pairs(text)
|
21
|
+
last_pair_found = false
|
22
|
+
|
23
|
+
text.each_line do |line|
|
24
|
+
|
25
|
+
unless last_pair_found
|
26
|
+
possible_pair = detect_pair(line)
|
27
|
+
if possible_pair and possible_pair.captures.length == 2
|
28
|
+
key = possible_pair.captures[0].downcase.to_sym
|
29
|
+
value = possible_pair.captures[1]
|
30
|
+
number_of_lines = value.count("\n") + 1
|
31
|
+
@result << { key => value.strip }
|
32
|
+
next # don't put this in the remainder
|
33
|
+
else
|
34
|
+
last_pair_found = true
|
35
|
+
end#of if
|
36
|
+
end
|
37
|
+
|
38
|
+
@remainder << line # keep this around, it might be useful
|
39
|
+
end#of each_line
|
40
|
+
|
41
|
+
@remainder.strip!
|
42
|
+
end#of parse_pairs
|
43
|
+
|
44
|
+
def detect_pair(line)
|
45
|
+
line.strip.match(/(^[A-Za-z0-9_]+):(.+)/)
|
46
|
+
end
|
47
|
+
|
48
|
+
def eval_primary_field
|
49
|
+
unless @remainder.blank? || @type.primary_field.blank?
|
50
|
+
@primary = @remainder
|
51
|
+
|
52
|
+
eval_heading_field unless @type.heading_field.blank?
|
53
|
+
|
54
|
+
save_primary
|
55
|
+
save_heading
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def eval_heading_field
|
60
|
+
# check for an <h1> at the beginning of the primary and if it's there pull it out for the heading
|
61
|
+
possible_hone = remainder.match(/(.+)\n=+\n*|^# (.+)\w*\n*/)
|
62
|
+
|
63
|
+
caps = []
|
64
|
+
caps = possible_hone.captures.compact unless possible_hone.blank?
|
65
|
+
|
66
|
+
# if it's there, then parse it out of the remainder
|
67
|
+
if !possible_hone.blank? and caps.length == 1
|
68
|
+
@heading = caps.first
|
69
|
+
|
70
|
+
### Remove heading from text
|
71
|
+
@primary = @primary.
|
72
|
+
strip.
|
73
|
+
gsub(/^#{possible_hone.captures.first}\n=+\n*/, '').
|
74
|
+
gsub(/^# #{possible_hone.captures.first}\w*\n*/, '').
|
75
|
+
strip
|
76
|
+
end#of if
|
77
|
+
end#of eval_heading_field
|
78
|
+
|
79
|
+
def save_primary
|
80
|
+
unless @primary.blank?
|
81
|
+
# see if a pair already exists for the primary_field
|
82
|
+
existing_primary = @result.select { |pair| pair.keys.first == @type.primary_field }
|
83
|
+
existing_primary = existing_primary.first
|
84
|
+
|
85
|
+
# get rid of the primary_field if it's already there
|
86
|
+
@result.delete(existing_primary)
|
87
|
+
|
88
|
+
# if there was any existing, prepend it to remanding
|
89
|
+
@primary = existing_primary.to_s + @primary unless existing_primary.blank?
|
90
|
+
|
91
|
+
# append a new pair for the fields
|
92
|
+
@result << { @type.primary_field => @primary } unless @primary.blank?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_heading
|
97
|
+
unless @heading.blank?
|
98
|
+
# see if a pair already exists for the primary_field
|
99
|
+
existing_heading = @result.select { |pair| pair.keys.first == @type.heading_field }
|
100
|
+
existing_heading = existing_heading.first
|
101
|
+
|
102
|
+
# get rid of the primary_field if it's already there
|
103
|
+
@result.delete(existing_heading)
|
104
|
+
|
105
|
+
# if there was any existing, prepend it to remanding
|
106
|
+
@heading = existing_heading.to_s + @heading unless existing_heading.blank?
|
107
|
+
|
108
|
+
# append a new pair for the fields
|
109
|
+
@result << { @type.heading_field => @heading } unless @heading.blank?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/lib/post_type.rb
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
require 'rdiscount' # markdown
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'uuidtools'
|
4
|
+
|
5
|
+
class PostType
|
6
|
+
extend ClassLevelInheritableAttributes
|
7
|
+
include Extlib::Hook
|
8
|
+
|
9
|
+
DEFAULT_FIELDS = [:published_at, :status, :slug, :trackbacks, :type, :tags]
|
10
|
+
|
11
|
+
# vars to inherit down
|
12
|
+
cattr_inheritable :fields_list, :allowed_fields_list, :required_fields_list, :primary_field, :heading_field,
|
13
|
+
:specials_blocks, :defaults_blocks, :only_declared_fields, :always_use_uuid,
|
14
|
+
:truncate_slugs, :markdown_fields
|
15
|
+
|
16
|
+
# defaults for class instance inheritable vars
|
17
|
+
@fields_list = []
|
18
|
+
@allowed_fields_list = []
|
19
|
+
@required_fields_list = []
|
20
|
+
@primary_field = nil
|
21
|
+
@heading_field = nil
|
22
|
+
@specials_blocks = {}
|
23
|
+
@defaults_blocks = {}
|
24
|
+
@only_declared_fields = true
|
25
|
+
@always_use_uuid = false
|
26
|
+
@truncate_slugs = true
|
27
|
+
@markdown_fields = []
|
28
|
+
|
29
|
+
|
30
|
+
### cattr_accessor
|
31
|
+
@@preferred_order = []
|
32
|
+
|
33
|
+
def self.preferred_order
|
34
|
+
@@preferred_order
|
35
|
+
end
|
36
|
+
def self.preferred_order=(new_order)
|
37
|
+
@@preferred_order = new_order
|
38
|
+
end
|
39
|
+
### cattr_accessor
|
40
|
+
|
41
|
+
attr_accessor :content # where everything is stored
|
42
|
+
|
43
|
+
### basic setup methods for types of posts
|
44
|
+
def self.fields(*list) # NOTE: this is a replacing function, not addititve like the others
|
45
|
+
self.fields_list = list.make_attrs
|
46
|
+
self.allowed_fields_list = [DEFAULT_FIELDS, list.make_attrs].flatten.uniq
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.allow(*list)
|
50
|
+
self.allowed_fields_list = [self.allowed_fields_list, list.make_attrs].flatten.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.required(*list)
|
54
|
+
self.required_fields_list = [
|
55
|
+
self.required_fields_list,
|
56
|
+
list.make_attrs.reject { |l| !fields_list.include? l }
|
57
|
+
].flatten.uniq
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.primary(field)
|
61
|
+
field = field.make_attr
|
62
|
+
|
63
|
+
if fields_list.include? field
|
64
|
+
self.primary_field = field
|
65
|
+
markdown field # primary is a markdown field by default
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.heading(field)
|
70
|
+
field = field.make_attr
|
71
|
+
self.heading_field = field if fields_list.include? field
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.special(field, &block)
|
75
|
+
field = field.make_attr
|
76
|
+
self.specials_blocks[field.make_attr] = block if fields_list.include? field
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.default(field, &block)
|
80
|
+
field = field.make_attr
|
81
|
+
self.defaults_blocks[field.make_attr] = block if fields_list.include? field
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.dynamic(field, &block)
|
85
|
+
field = field.make_attr
|
86
|
+
self.dynamic_blocks[field] = block
|
87
|
+
allow(field)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.markdown(*list)
|
91
|
+
self.markdown_fields = [
|
92
|
+
self.markdown_fields,
|
93
|
+
list.make_attrs.reject { |l| !fields_list.include? l }
|
94
|
+
].flatten.uniq
|
95
|
+
|
96
|
+
self.allowed_fields_list = [
|
97
|
+
self.allowed_fields_list,
|
98
|
+
self.markdown_fields.collect { |m| m.html }
|
99
|
+
].flatten.uniq
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_attr(key, value)
|
103
|
+
key = key.make_attr
|
104
|
+
|
105
|
+
unless value.blank?
|
106
|
+
@content[key] = value
|
107
|
+
|
108
|
+
if self.class.markdown_fields.include?(key)
|
109
|
+
markdown = Markdown.new @content[key].strip
|
110
|
+
@content[key.html] = markdown.to_html.strip
|
111
|
+
else
|
112
|
+
@content.delete(key.html)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
@content.delete(key)
|
116
|
+
@content.delete(key.html)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_default(key, value)
|
121
|
+
set_attr(key, value) if blank_attr?(key)
|
122
|
+
end
|
123
|
+
|
124
|
+
def blank_attr?(key)
|
125
|
+
get_attr(key).blank?
|
126
|
+
end
|
127
|
+
|
128
|
+
alias :get_attr? :blank_attr?
|
129
|
+
|
130
|
+
def get_attr(key, html = true)
|
131
|
+
key = key.make_attr
|
132
|
+
|
133
|
+
if html && self.class.markdown_fields.include?(key)
|
134
|
+
@content[key.html]
|
135
|
+
else
|
136
|
+
@content[key]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def delete_attr(key)
|
141
|
+
set_attr(key.make_attr, nil)
|
142
|
+
end
|
143
|
+
|
144
|
+
alias :remove_attr :delete_attr
|
145
|
+
alias :del_attr :delete_attr
|
146
|
+
|
147
|
+
# can be overriden to provide auto detection of type from a block of text
|
148
|
+
#
|
149
|
+
# Examples:
|
150
|
+
# def self.detect?(text)
|
151
|
+
# has_keys? text, :title, :body
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# def self.detect?(text)
|
155
|
+
# has_required? text
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# def self.detect?(text)
|
159
|
+
# has_one_or_more? text, :me
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
def self.detect?(text)
|
163
|
+
false
|
164
|
+
end
|
165
|
+
|
166
|
+
# useful for detection
|
167
|
+
def self.has_keys?(text, *fields)
|
168
|
+
needed = fields.make_attrs
|
169
|
+
get_pairs_count(text, needed).length == needed.length
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.has_more_than_one?(text, field)
|
173
|
+
has_more_than? text, field, 1
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.has_one_or_more?(text, field)
|
177
|
+
has_more_than? text, field, 0
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.has_more_than?(text, field, amount)
|
181
|
+
get_pairs_count(text, [field]).length > amount
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.get_pairs(text)
|
185
|
+
TextImporter.new(self).import(text)
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.get_pairs_count(text, fields)
|
189
|
+
pairs = get_pairs(text)
|
190
|
+
pairs.reject { |pair| !fields.include?(pair.keys.first) }
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.has_required?(text)
|
194
|
+
has_keys? text, *self.required_fields_list
|
195
|
+
end
|
196
|
+
|
197
|
+
# runs through the list of children looking for one that will work
|
198
|
+
def self.auto_detect(text)
|
199
|
+
list = self.preferred_order.blank? ? @all_children : self.preferred_order
|
200
|
+
|
201
|
+
list.each { |l| return l.new(text) if l.detect?(text) }
|
202
|
+
end
|
203
|
+
|
204
|
+
def content=(stuff) # !> method redefined; discarding old content=
|
205
|
+
@content = { :type => self.class.name.to_s }
|
206
|
+
import(stuff)
|
207
|
+
eval_specials
|
208
|
+
eval_defaults
|
209
|
+
parse_tags unless blank_attr?(:tags)
|
210
|
+
generate_slug if get_attr?(:slug)
|
211
|
+
@content
|
212
|
+
end#of content=
|
213
|
+
|
214
|
+
def valid? # TODO: this doesn't work if there are no required fields and the slug is not unique
|
215
|
+
v = true
|
216
|
+
|
217
|
+
if self.class.required_fields_list.blank?
|
218
|
+
v = false unless self.class.required_fields_list.reject { |item| !get_attr(item).blank? }.blank?
|
219
|
+
end
|
220
|
+
|
221
|
+
v = false unless slug_is_unique
|
222
|
+
|
223
|
+
v
|
224
|
+
end
|
225
|
+
|
226
|
+
def initialize(text = nil)
|
227
|
+
if text
|
228
|
+
self.content = text
|
229
|
+
# sanitize_content_fields
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def save
|
234
|
+
if valid?
|
235
|
+
truncate_slug if self.class.truncate_slugs
|
236
|
+
fill_default_fields
|
237
|
+
send_to_storage
|
238
|
+
else
|
239
|
+
false
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# TODO: how to determine the type of stuff? (text, json, yaml, image, video, photo, pdf, generic download file (can lookup type of file for icon if needed))
|
244
|
+
def import(stuff, type = :text)
|
245
|
+
importer = Kernel.const_get(type.to_s.camel_case+'Importer').new(self.class)
|
246
|
+
|
247
|
+
# The result sent back by an importer is either:
|
248
|
+
# Array:
|
249
|
+
# [{ :one => 'stuff' }, { :two => 'stuff' }]
|
250
|
+
# Hash:
|
251
|
+
# { :one => 'stuff', :two => 'stuff' }
|
252
|
+
result = importer.import(stuff)
|
253
|
+
|
254
|
+
case result
|
255
|
+
when Array
|
256
|
+
commit_array(result)
|
257
|
+
when Hash
|
258
|
+
commit_hash(result)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def commit_hash(pairs_hash)
|
263
|
+
pairs_hash.each do |key, value|
|
264
|
+
set_attr(key, value)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def commit_array(pairs_array)
|
269
|
+
pairs_array.each do |pairs_hash|
|
270
|
+
commit_hash(pairs_hash)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def eval_defaults
|
275
|
+
if valid?
|
276
|
+
self.class.defaults_blocks.each do |key, block|
|
277
|
+
set_default(key, self.instance_eval(&block))
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def eval_specials
|
283
|
+
self.class.specials_blocks.each do |key, block|
|
284
|
+
unless get_attr(key).blank?
|
285
|
+
set_attr(key, block.call(get_attr(key)))
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def parse_tags
|
291
|
+
if get_attr(:tags).class == String
|
292
|
+
tags_array = get_attr(:tags).split(',').collect { |t| t.strip }
|
293
|
+
set_attr(:tags, tags_array)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def sanitize_content_fields
|
298
|
+
@content.reject! { |key, value| !self.class.allowed_fields_list.include?(key) }
|
299
|
+
end
|
300
|
+
|
301
|
+
def send_to_storage # send to the db or whatever
|
302
|
+
false
|
303
|
+
end
|
304
|
+
|
305
|
+
def slug_is_unique # validate uniqueness
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
def fill_default_fields
|
310
|
+
set_default(:published_at, Time.now.utc)
|
311
|
+
set_default(:status, default_status)
|
312
|
+
end
|
313
|
+
|
314
|
+
def generate_slug # OPTIMIZE: this slug generation is ugly
|
315
|
+
result = ''
|
316
|
+
|
317
|
+
unless self.class.always_use_uuid
|
318
|
+
result = get_attr(self.class.heading_field, false).to_s.dup unless self.class.heading_field.blank?
|
319
|
+
|
320
|
+
if result.blank?
|
321
|
+
result = get_attr(self.class.primary_field, false).to_s.dup unless self.class.primary_field.blank?
|
322
|
+
end
|
323
|
+
|
324
|
+
if result.blank?
|
325
|
+
self.class.required_fields_list.each do |required_field|
|
326
|
+
unless get_attr(required_field).blank?
|
327
|
+
result = get_attr(required_field).to_s.dup
|
328
|
+
break
|
329
|
+
end#of unless
|
330
|
+
end#of each
|
331
|
+
end#of if
|
332
|
+
|
333
|
+
result.slugify!
|
334
|
+
end#of unless
|
335
|
+
|
336
|
+
if result.blank?
|
337
|
+
result = uuid
|
338
|
+
end
|
339
|
+
|
340
|
+
set_attr(:slug, result)
|
341
|
+
end
|
342
|
+
|
343
|
+
def truncate_slug(letter_count = 50)
|
344
|
+
unless get_attr(:slug).blank?
|
345
|
+
new_slug = get_attr(:slug).gsub(/^(.{#{letter_count}})(.*)/) { $1.slugify }
|
346
|
+
set_attr(:slug, new_slug)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def default_status
|
351
|
+
:published
|
352
|
+
end
|
353
|
+
|
354
|
+
def post_to_trackbacks
|
355
|
+
false
|
356
|
+
end
|
357
|
+
|
358
|
+
def uuid
|
359
|
+
UUID.timestamp_create.to_s
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
data/lib/types/audio.rb
ADDED
data/lib/types/chat.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
class Chat < PostType
|
2
|
+
fields :transcript
|
3
|
+
required :transcript
|
4
|
+
|
5
|
+
# override commit_pairs to make chat transcripts form the Me: pairs stuff
|
6
|
+
def commit_array(pairs_array)
|
7
|
+
transcript = []
|
8
|
+
|
9
|
+
pairs_array.each do |pairs_hash|
|
10
|
+
|
11
|
+
pairs_hash.each do |key, value|
|
12
|
+
|
13
|
+
unless self.class.allowed_fields_list.include?(key)
|
14
|
+
transcript << { key => value }
|
15
|
+
else
|
16
|
+
set_attr(key, value)
|
17
|
+
end
|
18
|
+
end#of each
|
19
|
+
|
20
|
+
end#of each
|
21
|
+
|
22
|
+
set_attr(:transcript, transcript)
|
23
|
+
|
24
|
+
end#of commit_pairs
|
25
|
+
|
26
|
+
def self.detect?(text)
|
27
|
+
has_one_or_more? text, :me
|
28
|
+
end
|
29
|
+
end#of Chat
|
data/lib/types/link.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Link < PostType
|
2
|
+
fields :url, :title, :description
|
3
|
+
required :url
|
4
|
+
primary :description
|
5
|
+
heading :title
|
6
|
+
|
7
|
+
special :url do |link_content|
|
8
|
+
'http://' + link_content.gsub(/^http:\/\//, '')
|
9
|
+
end
|
10
|
+
|
11
|
+
default :title do
|
12
|
+
result = ''
|
13
|
+
|
14
|
+
begin
|
15
|
+
doc = Nokogiri::HTML(open(get_attr(:url)))
|
16
|
+
doc.css('title').each { |t| result = t.content }
|
17
|
+
rescue Exception => e
|
18
|
+
end
|
19
|
+
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.detect?(text)
|
24
|
+
has_required? text
|
25
|
+
end
|
26
|
+
end
|
data/lib/types/photo.rb
ADDED
data/lib/types/quote.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class Quote < PostType
|
2
|
+
fields :quote, :source
|
3
|
+
required :quote
|
4
|
+
primary :quote
|
5
|
+
|
6
|
+
special :quote do |quote_content|
|
7
|
+
unless (quote_content =~ /^<blockquote/).nil?
|
8
|
+
doc = Nokogiri::HTML(quote_content)
|
9
|
+
doc.css('blockquote').each { |q| quote_content = q.content }
|
10
|
+
else
|
11
|
+
quote_content = ''
|
12
|
+
end
|
13
|
+
quote_content.strip
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.detect?(text)
|
17
|
+
pairs = get_pairs(text)
|
18
|
+
|
19
|
+
the_quote = pairs.select { |pair| pair.keys.first == :quote }
|
20
|
+
the_quote = the_quote.first || {}
|
21
|
+
|
22
|
+
markdown = Markdown.new(the_quote[:quote])
|
23
|
+
quote_html = markdown.to_html.strip
|
24
|
+
|
25
|
+
!(text =~ /Quote: (.*)/).nil? || !(quote_html =~ /^<blockquote(.+)<\/blockquote>$/m).nil?
|
26
|
+
end
|
27
|
+
end
|
data/lib/types/review.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class Review < PostType
|
2
|
+
fields :rating, :item, :description
|
3
|
+
required :rating, :item
|
4
|
+
primary :description
|
5
|
+
heading :item
|
6
|
+
|
7
|
+
special :rating do |rating_content|
|
8
|
+
rating_content.to_f
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.detect?(text)
|
12
|
+
has_required? text
|
13
|
+
end
|
14
|
+
end
|
data/lib/types/video.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe PostType do
|
4
|
+
|
5
|
+
before do
|
6
|
+
class Yellow; end
|
7
|
+
Object.send(:remove_const, :Yellow)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "have empty content when new" do
|
11
|
+
PostType.new.content.should == nil
|
12
|
+
end
|
13
|
+
|
14
|
+
should "have a preferred order array" do
|
15
|
+
PostType.preferred_order.should == [Video, Audio, Photo, Chat, Review, Link, Quote, Article]
|
16
|
+
end
|
17
|
+
|
18
|
+
should "set fields" do
|
19
|
+
class Yellow < PostType
|
20
|
+
fields :one, :two
|
21
|
+
end
|
22
|
+
|
23
|
+
Yellow.fields_list.should == [:one, :two]
|
24
|
+
end
|
25
|
+
|
26
|
+
should "set allowed fields" do
|
27
|
+
class Yellow < PostType
|
28
|
+
fields :one
|
29
|
+
allow :anyone, :noone
|
30
|
+
end
|
31
|
+
|
32
|
+
Yellow.allowed_fields_list.should == [Yellow::DEFAULT_FIELDS, :one, :anyone, :noone].flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
should "set required fields" do
|
36
|
+
class Yellow < PostType
|
37
|
+
fields :one, :two, :three
|
38
|
+
required :one, :two
|
39
|
+
end
|
40
|
+
|
41
|
+
Yellow.required_fields_list.should == [:one, :two]
|
42
|
+
end
|
43
|
+
|
44
|
+
should "set primary field" do
|
45
|
+
class Yellow < PostType
|
46
|
+
fields :one, :two
|
47
|
+
primary :one
|
48
|
+
end
|
49
|
+
|
50
|
+
Yellow.primary_field.should == :one
|
51
|
+
end
|
52
|
+
|
53
|
+
# should use primary field
|
54
|
+
# should make primary field markdown automatically
|
55
|
+
|
56
|
+
should "set heading field" do
|
57
|
+
class Yellow < PostType
|
58
|
+
fields :one, :two
|
59
|
+
heading :one
|
60
|
+
end
|
61
|
+
|
62
|
+
Yellow.heading_field.should == :one
|
63
|
+
end
|
64
|
+
|
65
|
+
should "use heading field" do
|
66
|
+
class Yellow < PostType
|
67
|
+
fields :one, :two
|
68
|
+
primary :one
|
69
|
+
heading :two
|
70
|
+
end
|
71
|
+
|
72
|
+
y = Yellow.new "Hello Two
|
73
|
+
=========
|
74
|
+
|
75
|
+
This will be in One."
|
76
|
+
|
77
|
+
y.get_attr(:one).should == "<p>This will be in One.</p>"
|
78
|
+
y.get_attr(:two).should == "Hello Two"
|
79
|
+
end
|
80
|
+
|
81
|
+
should "set special values" do
|
82
|
+
class Yellow < PostType
|
83
|
+
fields :plus_one, :two
|
84
|
+
|
85
|
+
special :plus_one do |field_value|
|
86
|
+
field_value + 1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
Yellow.specials_blocks[:plus_one].should.not.be.blank?
|
91
|
+
end
|
92
|
+
|
93
|
+
should "use special values" do
|
94
|
+
class Yellow < PostType
|
95
|
+
fields :one, :two
|
96
|
+
|
97
|
+
special :one do |field_value|
|
98
|
+
field_value.to_i + 1
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
y = Yellow.new "One: 2\n\nhello"
|
103
|
+
|
104
|
+
y.get_attr(:one).should == 3
|
105
|
+
end
|
106
|
+
|
107
|
+
should "set default values" do
|
108
|
+
class Yellow < PostType
|
109
|
+
fields :one, :two
|
110
|
+
|
111
|
+
default(:one) { 1 }
|
112
|
+
end
|
113
|
+
|
114
|
+
Yellow.defaults_blocks[:one].call.should == 1
|
115
|
+
end
|
116
|
+
|
117
|
+
should "use default values" do
|
118
|
+
class Yellow < PostType
|
119
|
+
fields :one, :two
|
120
|
+
|
121
|
+
default :two do
|
122
|
+
2
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
y = Yellow.new 'hello'
|
127
|
+
|
128
|
+
y.get_attr(:two).should == 2
|
129
|
+
end
|
130
|
+
|
131
|
+
should "use markdown fields" do
|
132
|
+
class Yellow < PostType
|
133
|
+
fields :one, :two
|
134
|
+
markdown :two
|
135
|
+
end
|
136
|
+
|
137
|
+
Yellow.markdown_fields.should == [:two]
|
138
|
+
Yellow.allowed_fields_list.should == [Yellow::DEFAULT_FIELDS, :one, :two, :two_html].flatten
|
139
|
+
end
|
140
|
+
|
141
|
+
should "use markdown fields" do
|
142
|
+
class Yellow < PostType
|
143
|
+
fields :one, :two
|
144
|
+
markdown :two
|
145
|
+
end
|
146
|
+
|
147
|
+
y = Yellow.new "Two: *hahaha*"
|
148
|
+
|
149
|
+
y.get_attr(:two).should == "<p><em>hahaha</em></p>"
|
150
|
+
end
|
151
|
+
|
152
|
+
should "allow setting and getting of an attribute of the content" do
|
153
|
+
class Yellow < PostType; end
|
154
|
+
y = Yellow.new 'stuff'
|
155
|
+
y.set_attr(:first, 'woo hoo')
|
156
|
+
y.get_attr(:first).should == 'woo hoo'
|
157
|
+
end
|
158
|
+
|
159
|
+
should "not keep empty attributes" do
|
160
|
+
class Yellow < PostType
|
161
|
+
fields :one
|
162
|
+
primary :one
|
163
|
+
end
|
164
|
+
|
165
|
+
y = Yellow.new 'stuff'
|
166
|
+
y.set_attr(:one, '')
|
167
|
+
y.get_attr(:one).should.be.nil
|
168
|
+
end
|
169
|
+
|
170
|
+
# should auto_detect ?
|
171
|
+
# should save ?
|
172
|
+
# should generate slug
|
173
|
+
|
174
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper.rb')
|
2
|
+
|
3
|
+
describe Chat do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@good_chat = "You: Hello\nMe: Hello back at ya!\nYou: Wanna go eat\nYou: Somehwere\nMe: Yes!"
|
7
|
+
@bad_chat = "Rating: 4\nItem: Dyson\n\nThis should be the description."
|
8
|
+
|
9
|
+
class Yellow; end
|
10
|
+
Object.send(:remove_const, :Yellow)
|
11
|
+
end
|
12
|
+
|
13
|
+
should "detect a well-formed chat" do
|
14
|
+
Chat.detect?(@good_chat).should.be.true
|
15
|
+
end
|
16
|
+
|
17
|
+
should "not detect a malformed chat" do
|
18
|
+
Chat.detect?(@bad_chat).should.be.false
|
19
|
+
end
|
20
|
+
|
21
|
+
should "autodetect a well-formed chat" do
|
22
|
+
PostType.auto_detect(@good_chat).should.be.kind_of Chat
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper.rb')
|
2
|
+
|
3
|
+
describe Link do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@good_link = "URL: google.com\n\nThis should be the description."
|
7
|
+
@bad_link = "Rating: 4\nItem: Dyson\n\nThis should be the description."
|
8
|
+
|
9
|
+
class Yellow; end
|
10
|
+
Object.send(:remove_const, :Yellow)
|
11
|
+
end
|
12
|
+
|
13
|
+
should "detect a well-formed link" do
|
14
|
+
Link.detect?(@good_link).should.be.true
|
15
|
+
end
|
16
|
+
|
17
|
+
should "not detect a malformed link" do
|
18
|
+
Link.detect?(@bad_link).should.be.false
|
19
|
+
end
|
20
|
+
|
21
|
+
should "autodetect a well-formed link" do
|
22
|
+
PostType.auto_detect(@good_link).should.be.kind_of Link
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper.rb')
|
2
|
+
|
3
|
+
describe Quote do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@good_quote = "> This should be a quote."
|
7
|
+
@good_quote_with_source = "Source: George Washington\n\n> This is what he said."
|
8
|
+
@good_quote_all_pairs = "Quote: Hello\nSource: Me"
|
9
|
+
@bad_quote = "Rating: 4\nItem: Dyson\n\nThis should be the description."
|
10
|
+
|
11
|
+
class Yellow; end
|
12
|
+
Object.send(:remove_const, :Yellow)
|
13
|
+
end
|
14
|
+
|
15
|
+
should "detect a well-formed quote through markdown" do
|
16
|
+
Quote.detect?(@good_quote).should.be.true
|
17
|
+
end
|
18
|
+
|
19
|
+
should "detect a well-formed quote with source" do
|
20
|
+
Quote.detect?(@good_quote_with_source).should.be.true
|
21
|
+
end
|
22
|
+
|
23
|
+
should "detect a well-formed quote if it's all pairs" do
|
24
|
+
Quote.detect?(@good_quote_all_pairs).should.be.true
|
25
|
+
end
|
26
|
+
|
27
|
+
should "not detect a malformed quote" do
|
28
|
+
Quote.detect?(@bad_quote).should.be.false
|
29
|
+
end
|
30
|
+
|
31
|
+
should "autodetect a well-formed quote" do
|
32
|
+
PostType.auto_detect(@good_quote).should.be.kind_of Quote
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../spec_helper.rb')
|
2
|
+
|
3
|
+
describe Review do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@good_review = "Rating: 4\nItem: Dyson\n\nThis should be the description."
|
7
|
+
@bad_review = "You: Hello\nMe: Hello back at ya!\nYou: Wanna go eat\nYou: Somehwere\nMe: Yes!"
|
8
|
+
@almost_good_review = "Rating: 4\n\nThis should be the description."
|
9
|
+
|
10
|
+
class Yellow; end
|
11
|
+
Object.send(:remove_const, :Yellow)
|
12
|
+
end
|
13
|
+
|
14
|
+
should "detect a well-formed review" do
|
15
|
+
Review.detect?(@good_review).should.be.true
|
16
|
+
end
|
17
|
+
|
18
|
+
should "not detect a malformed review" do
|
19
|
+
Review.detect?(@bad_review).should.be.false
|
20
|
+
end
|
21
|
+
|
22
|
+
should "not detect an almost good review" do
|
23
|
+
Review.detect?(@almost_good_review).should.be.false
|
24
|
+
end
|
25
|
+
|
26
|
+
should "autodetect a well-formed review" do
|
27
|
+
PostType.auto_detect(@good_review).should.be.kind_of Review
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/spec/spec.opts
ADDED
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require 'bacon'
|
3
|
+
require 'mocha'
|
4
|
+
|
5
|
+
require File.expand_path(File.join(__FILE__.split('/spec').first, 'turbine-core.rb'))
|
6
|
+
|
7
|
+
PostType.preferred_order = [Video, Audio, Photo, Chat, Review, Link, Quote, Article]
|
8
|
+
|
9
|
+
### run specs with `bacon spec/**/*.rb spec/*.rb`
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
# from 1 to 10 how likely, 1 being not very likely and 10 being all the time
|
14
|
+
def do_i?(i = 5)
|
15
|
+
rand(500) < 50 * i
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
# Hash additions.
|
23
|
+
#
|
24
|
+
# From
|
25
|
+
# * http://wincent.com/knowledge-base/Fixtures_considered_harmful%3F
|
26
|
+
# * Neil Rahilly
|
27
|
+
|
28
|
+
class Hash
|
29
|
+
|
30
|
+
##
|
31
|
+
# Filter keys out of a Hash.
|
32
|
+
#
|
33
|
+
# { :a => 1, :b => 2, :c => 3 }.except(:a)
|
34
|
+
# => { :b => 2, :c => 3 }
|
35
|
+
|
36
|
+
def except(*keys)
|
37
|
+
self.reject { |k,v| keys.include?(k || k.to_sym) }
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Override some keys.
|
42
|
+
#
|
43
|
+
# { :a => 1, :b => 2, :c => 3 }.with(:a => 4)
|
44
|
+
# => { :a => 4, :b => 2, :c => 3 }
|
45
|
+
|
46
|
+
def with(overrides = {})
|
47
|
+
self.merge overrides
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Returns a Hash with only the pairs identified by +keys+.
|
52
|
+
#
|
53
|
+
# { :a => 1, :b => 2, :c => 3 }.only(:a)
|
54
|
+
# => { :a => 1 }
|
55
|
+
|
56
|
+
def only(*keys)
|
57
|
+
self.reject { |k,v| !keys.include?(k || k.to_sym) }
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: myobie-turbine-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathan Herald
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: TODO
|
17
|
+
email: nathan@myobie.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- Rakefile
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/ext.rb
|
28
|
+
- lib/importers/json_importer.rb
|
29
|
+
- lib/importers/text_importer.rb
|
30
|
+
- lib/post_type.rb
|
31
|
+
- lib/types/article.rb
|
32
|
+
- lib/types/audio.rb
|
33
|
+
- lib/types/chat.rb
|
34
|
+
- lib/types/link.rb
|
35
|
+
- lib/types/photo.rb
|
36
|
+
- lib/types/quote.rb
|
37
|
+
- lib/types/review.rb
|
38
|
+
- lib/types/video.rb
|
39
|
+
- spec/post_type_spec.rb
|
40
|
+
- spec/post_types/chat_spec.rb
|
41
|
+
- spec/post_types/link_spec.rb
|
42
|
+
- spec/post_types/quote_spec.rb
|
43
|
+
- spec/post_types/review_spec.rb
|
44
|
+
- spec/spec.opts
|
45
|
+
- spec/spec_helper.rb
|
46
|
+
- README
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/myobie/turbine-core
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.2.0
|
70
|
+
signing_key:
|
71
|
+
specification_version: 2
|
72
|
+
summary: TODO
|
73
|
+
test_files:
|
74
|
+
- spec/post_type_spec.rb
|
75
|
+
- spec/post_types/chat_spec.rb
|
76
|
+
- spec/post_types/link_spec.rb
|
77
|
+
- spec/post_types/quote_spec.rb
|
78
|
+
- spec/post_types/review_spec.rb
|
79
|
+
- spec/spec_helper.rb
|