typingpool 0.7.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.
Files changed (90) hide show
  1. data/Rakefile +23 -0
  2. data/bin/tp-assign +240 -0
  3. data/bin/tp-collect +50 -0
  4. data/bin/tp-config +114 -0
  5. data/bin/tp-finish +101 -0
  6. data/bin/tp-make +169 -0
  7. data/bin/tp-review +175 -0
  8. data/lib/typingpool/amazon.rb +732 -0
  9. data/lib/typingpool/app.rb +634 -0
  10. data/lib/typingpool/config.rb +344 -0
  11. data/lib/typingpool/error.rb +22 -0
  12. data/lib/typingpool/filer.rb +396 -0
  13. data/lib/typingpool/project.rb +593 -0
  14. data/lib/typingpool/template.rb +175 -0
  15. data/lib/typingpool/templates/assignment/amazon-init.js +38 -0
  16. data/lib/typingpool/templates/assignment/interview/nameless.html.erb +13 -0
  17. data/lib/typingpool/templates/assignment/interview/noisy.html.erb +12 -0
  18. data/lib/typingpool/templates/assignment/interview/partials/voices.html.erb +10 -0
  19. data/lib/typingpool/templates/assignment/interview/phone.html.erb +12 -0
  20. data/lib/typingpool/templates/assignment/interview.html.erb +11 -0
  21. data/lib/typingpool/templates/assignment/main.css +20 -0
  22. data/lib/typingpool/templates/assignment/partials/entry.html.erb +19 -0
  23. data/lib/typingpool/templates/assignment/partials/footer.html.erb +3 -0
  24. data/lib/typingpool/templates/assignment/partials/header.html.erb +11 -0
  25. data/lib/typingpool/templates/assignment/partials/labeling-example.html.erb +4 -0
  26. data/lib/typingpool/templates/assignment/partials/labeling.html.erb +5 -0
  27. data/lib/typingpool/templates/assignment/partials/length-description.html.erb +6 -0
  28. data/lib/typingpool/templates/assignment/partials/voices.html.erb +10 -0
  29. data/lib/typingpool/templates/assignment/speech.html.erb +11 -0
  30. data/lib/typingpool/templates/config.yml +21 -0
  31. data/lib/typingpool/templates/project/audio/chunks/.empty_directory +0 -0
  32. data/lib/typingpool/templates/project/audio/originals/.empty_directory +0 -0
  33. data/lib/typingpool/templates/project/data/.empty_directory +0 -0
  34. data/lib/typingpool/templates/project/etc/ About these files - read me.txt +8 -0
  35. data/lib/typingpool/templates/project/etc/audio-compat.js +25 -0
  36. data/lib/typingpool/templates/project/etc/player/audio-player.js +4 -0
  37. data/lib/typingpool/templates/project/etc/player/license.txt +19 -0
  38. data/lib/typingpool/templates/project/etc/player/player.swf +0 -0
  39. data/lib/typingpool/templates/project/etc/transcript.css +49 -0
  40. data/lib/typingpool/templates/transcript.html.erb +23 -0
  41. data/lib/typingpool/test/fixtures/amazon-question-html.html +95 -0
  42. data/lib/typingpool/test/fixtures/amazon-question-url.txt +1 -0
  43. data/lib/typingpool/test/fixtures/audio/mp3/interview.1.mp3 +0 -0
  44. data/lib/typingpool/test/fixtures/audio/mp3/interview.2.mp3 +0 -0
  45. data/lib/typingpool/test/fixtures/audio/wma/VN620007.WMA +0 -0
  46. data/lib/typingpool/test/fixtures/audio/wma/VN620052.WMA +0 -0
  47. data/lib/typingpool/test/fixtures/config-1 +20 -0
  48. data/lib/typingpool/test/fixtures/config-2 +25 -0
  49. data/lib/typingpool/test/fixtures/not_yaml.txt +4 -0
  50. data/lib/typingpool/test/fixtures/template-2.html.erb +10 -0
  51. data/lib/typingpool/test/fixtures/template-3.html.erb +22 -0
  52. data/lib/typingpool/test/fixtures/template.html.erb +10 -0
  53. data/lib/typingpool/test/fixtures/tp_collect_id.txt +1 -0
  54. data/lib/typingpool/test/fixtures/tp_collect_sandbox-assignment.csv +8 -0
  55. data/lib/typingpool/test/fixtures/tp_review_id.txt +1 -0
  56. data/lib/typingpool/test/fixtures/tp_review_sandbox-assignment.csv +8 -0
  57. data/lib/typingpool/test/fixtures/transcript-chunks.csv +226 -0
  58. data/lib/typingpool/test/fixtures/utf8_transcript.txt +7 -0
  59. data/lib/typingpool/test/fixtures/vcr/tp-collect-1.yml +2712 -0
  60. data/lib/typingpool/test/fixtures/vcr/tp-collect-2.yml +2718 -0
  61. data/lib/typingpool/test/fixtures/vcr/tp-collect-3.yml +2768 -0
  62. data/lib/typingpool/test/fixtures/vcr/tp-review-1.yml +570 -0
  63. data/lib/typingpool/test/fixtures/vcr/tp-review-2.yml +351 -0
  64. data/lib/typingpool/test.rb +418 -0
  65. data/lib/typingpool/transcript.rb +181 -0
  66. data/lib/typingpool/utility.rb +272 -0
  67. data/lib/typingpool.rb +500 -0
  68. data/test/make_amazon_question_fixture.rb +24 -0
  69. data/test/make_tp_collect_fixture_1.rb +26 -0
  70. data/test/make_tp_collect_fixture_2.rb +16 -0
  71. data/test/make_tp_collect_fixture_3.rb +15 -0
  72. data/test/make_tp_collect_fixture_4.rb +17 -0
  73. data/test/make_tp_review_fixture_1.rb +26 -0
  74. data/test/make_tp_review_fixture_2.rb +30 -0
  75. data/test/make_transcript_chunks_fixture.rb +53 -0
  76. data/test/test_integration_script_1_tp_config.rb +108 -0
  77. data/test/test_integration_script_2_tp_make.rb +119 -0
  78. data/test/test_integration_script_3_tp_assign.rb +152 -0
  79. data/test/test_integration_script_4_tp_review.rb +72 -0
  80. data/test/test_integration_script_5_tp_collect.rb +44 -0
  81. data/test/test_integration_script_6_tp_finish.rb +123 -0
  82. data/test/test_unit_amazon.rb +153 -0
  83. data/test/test_unit_config.rb +94 -0
  84. data/test/test_unit_filer.rb +202 -0
  85. data/test/test_unit_project.rb +168 -0
  86. data/test/test_unit_project_local.rb +68 -0
  87. data/test/test_unit_project_remote.rb +157 -0
  88. data/test/test_unit_template.rb +111 -0
  89. data/test/test_unit_transcript.rb +77 -0
  90. metadata +234 -0
@@ -0,0 +1,272 @@
1
+ module Typingpool
2
+ module Utility
3
+ require 'open3'
4
+ require 'uri'
5
+ require 'tmpdir'
6
+ require 'set'
7
+ require 'net/http'
8
+
9
+ class << self
10
+ #Much like Kernel#system, except it doesn't spew STDERR and
11
+ #STDOUT all over your screen (when called with multiple args,
12
+ #which with Kernel#systems kills the chance to do shell style
13
+ #stream redirects like 2>/dev/null). Even more like
14
+ #Open3.capture3, except it raises an exception on unsuccesful
15
+ #exit status.
16
+ #
17
+ # ==== Params
18
+ #[cmd] Commands to send to the shell, just as with Kernel#system.
19
+ #
20
+ # ==== Returns
21
+ #On success: STDOUT, or true if STDOUT is empty
22
+ #On failure: Raises Typingpool::Error::Shell, with STDERR as
23
+ #error text if available.
24
+ def system_quietly(*cmd)
25
+ out, err, status = Open3.capture3(*cmd)
26
+ if status.success?
27
+ return out ? out.chomp : true
28
+ else
29
+ if err
30
+ raise Error::Shell, err.chomp
31
+ else
32
+ raise Error::Shell
33
+ end
34
+ end
35
+ end
36
+
37
+ #Convert config entries like '30s','2d','10h'. etc into number of seconds.
38
+ #
39
+ # ==== Params
40
+ #[timespec] string conforming to format outlined at
41
+ #http://search.cpan.org/~markstos/CGI-Session-4.48/lib/CGI/Session.pm#expire($param,_$time)
42
+ # ==== Returns
43
+ #Number of whole seconds corresponding to the timespec. Raises
44
+ #Typingpool::Error::Argument::Format on bad input.
45
+ def timespec_to_seconds(timespec)
46
+ timespec or return
47
+ suffix_to_time = {
48
+ 's'=>1,
49
+ 'm'=>60,
50
+ 'h'=>60*60,
51
+ 'd'=>60*60*24,
52
+ 'M'=>60*60*24*30,
53
+ 'y'=>60*60*24*365
54
+ }
55
+ match = timespec.to_s.match(/^\+?(\d+(\.\d+)?)\s*([#{suffix_to_time.keys.join}])?$/) or raise Error::Argument::Format, "Can't convert '#{timespec}' to time"
56
+ suffix = match[3] || 's'
57
+ return (match[1].to_f * suffix_to_time[suffix].to_i).to_i
58
+ end
59
+
60
+ # ==== Returns
61
+ #Single hash, with keys corresponding to Headers and values
62
+ #corresponding to respective entries in Array.
63
+ def array_to_hash(array, headers)
64
+ Hash[*headers.zip(array).flatten]
65
+ end
66
+
67
+ #The base file at the root of the URL path.
68
+ # ==== Params
69
+ #[url] string url
70
+ # ==== Returns
71
+ #File name
72
+ def url_basename(url)
73
+ File.basename(URI.parse(url).path)
74
+ end
75
+
76
+ #Converts standard linefeed combos - CRLF, CR, LF - to whatever
77
+ #the system newline is, aka "\n".
78
+ # ==== Returns
79
+ #String with normalized newlines
80
+ def normalize_newlines(text)
81
+ text.gsub!("\r\n", "\n")
82
+ text.gsub!("\r", "\n")
83
+ text.gsub!("\f", "\n")
84
+ text
85
+ end
86
+
87
+ #Does a natural-feeling conversion between plain text linebreaks
88
+ #and HTML P and BR tags, as typical with most web comment forms.
89
+ # ==== Returns
90
+ #Text with double newlines converted to P tags, single
91
+ #newlines converted to BR tags, and the original newlines
92
+ #restored.
93
+ def newlines_to_html(text)
94
+ text.gsub!(/\n\n+/, '<p>')
95
+ text.gsub!(/\n/, '<br>')
96
+ text.gsub!(/<p>/, "\n\n<p>")
97
+ text.gsub!(/<br>/, "\n<br>")
98
+ text
99
+ end
100
+
101
+ #Takes an array, returns a string with the array elements joined
102
+ #with a comma, except for the last and second to last items,
103
+ #which are joined with ' and '. For example: ['foo','bar'] => "foo
104
+ #and bar"; ['foo','bar','baz'] => "foo, bar and baz"
105
+ #
106
+ #Also takes an optional flag which specifies whether to use an
107
+ #oxford comma. Default is false. If set to true, the last and
108
+ #second to last items will be joined with ', and'
109
+ def join_in_english(array, oxford_comma=false)
110
+ array = array.dup
111
+ oxford_comma = (oxford_comma && array.count > 2) ? ',' : ''
112
+ last = array.pop
113
+ array.empty? ? last : [array.join(', '), last].join("#{oxford_comma} and ")
114
+ end
115
+
116
+ #Takes a block and calls that block with a path to a temporary
117
+ #directory. Recursively deletes that directory when the block is
118
+ #finished.
119
+ def in_temp_dir
120
+ dir = Dir.mktmpdir
121
+ begin
122
+ yield(dir)
123
+ ensure
124
+ FileUtils.remove_entry_secure(dir)
125
+ end # begin
126
+ end
127
+
128
+ #Returns Typingpool's lib/ root, usually for purposes of
129
+ #locating templates or test fixtures.
130
+ def lib_dir
131
+ File.dirname(__FILE__)
132
+ end
133
+
134
+ #Returns true if this Ruby was built on Mac OS X
135
+ def os_x?
136
+ RUBY_PLATFORM.match(/\bdarwin/i)
137
+ end
138
+
139
+ #Returns true if anything appears to be waiting on STDIN
140
+ def stdin_has_content?
141
+ STDIN.fcntl(Fcntl::F_GETFL, 0) == 0
142
+ end
143
+
144
+ #Makes one or more HEAD requests to determine whether a
145
+ #particular web resource is available.
146
+ # ==== Params
147
+ #[url] URL as a string.
148
+ #[max_redirects] Default 6. Maximum number of HTTP redirects to
149
+ # follow.
150
+ # ==== Returns
151
+ #True if the HTTP response code indicates success (after
152
+ #following redirects). False if the HTTP response code indicates
153
+ #an error (e.g. 4XX and 5XX response codes).
154
+ def working_url?(url, max_redirects=6)
155
+ response = request_url_with(url, max_redirects) do |url, http|
156
+ http.request_head(url.path)
157
+ end #request_url_with... do |url|
158
+ response.kind_of?(Net::HTTPSuccess)
159
+ end
160
+
161
+ #Makes one or more web requests to fetch a resource. Follows
162
+ #redirects by default.
163
+ # ==== Params
164
+ #[url] URL as a string.
165
+ #[max_redirects] Default 6. Maximum number of HTTP redirects to
166
+ # follow.
167
+ # ==== Exceptions
168
+ #Raises Error::HTTP if it receives an HTTP response code
169
+ #indicating an error (after followinf redirects). Exception
170
+ #message will include the response code and response message.
171
+ # ==== Returns
172
+ #A Net::HTTPResponse instance, if the request was successful.
173
+ def fetch_url(url, max_redirects=6)
174
+ response = request_url_with(url, max_redirects) do |url, http|
175
+ http.request_get(url.path)
176
+ end
177
+ if response.kind_of?(Net::HTTPSuccess)
178
+ return response
179
+ else
180
+ raise Error::HTTP, "HTTP error fetching '#{url.to_s}': '#{response.code}: #{response.message}'"
181
+ end #if response.kind_of?
182
+ end
183
+
184
+ #protected
185
+
186
+ def request_url_with(url, max_redirects=6)
187
+ seen = Set.new
188
+ loop do
189
+ url = URI.parse(url)
190
+ if seen.include? url.to_s
191
+ raise Error::HTTP, "Redirect infinite loop (at '#{url.to_s}')"
192
+ end
193
+ if seen.count > max_redirects
194
+ raise Error::HTTP, "Too many redirects (>#{max_redirects})"
195
+ end
196
+ seen.add(url.to_s)
197
+ #Die in a fire, net/http.
198
+ http = Net::HTTP.new(url.host, url.port)
199
+ http.use_ssl = true if url.scheme == 'https'
200
+ response = yield(url, http)
201
+ if response.kind_of?(Net::HTTPRedirection)
202
+ url = response['location']
203
+ else
204
+ return response
205
+ end #if response.kind_of?...
206
+ end #loop do
207
+ end
208
+
209
+ end #class << self
210
+
211
+ module Castable
212
+ #Cast this object instance to a relative class. Call this from
213
+ #super in your own class if you want to pass args to the
214
+ #relative class constructor. All args after the first will be
215
+ #passed to new.
216
+ #
217
+ #A relative class can be a subclass and in some cases a sibling
218
+ #class, parent class, parent sibling class, grandparent class,
219
+ #grandparent sibling class, and so on. A relative class will
220
+ #never be higher up the inheritance tree than the subclasses of
221
+ #the class where Castable was included.
222
+ # ==== Params
223
+ # [sym] Symbol corresponding to relative class to cast into. For
224
+ # example, Class#as(:audio) will cast into a Class::Audio
225
+ # and Class#as(:csv) will cast into Class::CSV. Casting
226
+ # is class insensitive, which means you can't have class
227
+ # CSV and class Csv. To cast into a related class whose
228
+ # name is not not directly under that of its parent, you
229
+ # must either specify the full name,
230
+ # e.g. Class#as(:foo_bar_baz) to cast to Foo::Bar::Baz,
231
+ # or a name relative to the parent,
232
+ # e.g. Class#as(:remote_html), where Class::Remote does
233
+ # not inherit from Class but Class::Remote::HTML does.
234
+ # ==== Returns
235
+ # New instance of subclass
236
+ def as(sym, *args)
237
+ if klass = self.class.relative_klass(sym.to_s.downcase)
238
+ klass.new(*args)
239
+ else
240
+ raise Error, "Can't find class '#{sym.to_s}' to cast to"
241
+ end #if subklass =...
242
+ end
243
+
244
+ def self.included(receiver)
245
+ receiver.extend(ClassMethods)
246
+ end
247
+
248
+ module ClassMethods
249
+ def inherited(subklass)
250
+ subklasses[subklass.to_s.split("#{self.name}::").last.downcase.gsub(/::/, '_')] = subklass
251
+ end
252
+
253
+ def subklasses
254
+ @subklasses ||= {}
255
+ end
256
+
257
+ def subklass(subklass_key)
258
+ subklasses[subklass_key]
259
+ end
260
+
261
+ def relative_klass(key)
262
+ if subklasses[key]
263
+ subklasses[key]
264
+ elsif self.superclass.respond_to? :relative_klass
265
+ self.superclass.relative_klass(key)
266
+ end
267
+ end
268
+
269
+ end #module ClassMethods
270
+ end #Castable
271
+ end #Utility
272
+ end #Typingpool