typingpool 0.7.0 → 0.7.1
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/LICENSE +20 -0
- data/README.markdown +452 -0
- data/lib/typingpool/amazon/hit/assignment/empty.rb +19 -0
- data/lib/typingpool/amazon/hit/assignment.rb +43 -0
- data/lib/typingpool/amazon/hit/full/fromsearchhits.rb +44 -0
- data/lib/typingpool/amazon/hit/full.rb +105 -0
- data/lib/typingpool/amazon/hit.rb +458 -0
- data/lib/typingpool/amazon/question.rb +45 -0
- data/lib/typingpool/amazon.rb +3 -677
- data/lib/typingpool/app/cli/formatter.rb +16 -0
- data/lib/typingpool/app/cli.rb +64 -0
- data/lib/typingpool/app/friendlyexceptions.rb +34 -0
- data/lib/typingpool/app.rb +2 -97
- data/lib/typingpool/config/root.rb +114 -0
- data/lib/typingpool/config.rb +13 -119
- data/lib/typingpool/filer/audio.rb +84 -0
- data/lib/typingpool/filer/csv.rb +57 -0
- data/lib/typingpool/filer/dir.rb +76 -0
- data/lib/typingpool/filer/files/audio.rb +63 -0
- data/lib/typingpool/filer/files.rb +55 -0
- data/lib/typingpool/filer.rb +4 -313
- data/lib/typingpool/project/local.rb +117 -0
- data/lib/typingpool/project/remote/s3.rb +135 -0
- data/lib/typingpool/project/remote/sftp.rb +100 -0
- data/lib/typingpool/project/remote.rb +65 -0
- data/lib/typingpool/project.rb +2 -396
- data/lib/typingpool/template/assignment.rb +17 -0
- data/lib/typingpool/template/env.rb +77 -0
- data/lib/typingpool/template.rb +2 -87
- data/lib/typingpool/test/script.rb +310 -0
- data/lib/typingpool/test.rb +1 -306
- data/lib/typingpool/transcript/chunk.rb +129 -0
- data/lib/typingpool/transcript.rb +1 -125
- data/lib/typingpool/utility/castable.rb +65 -0
- data/lib/typingpool/utility.rb +1 -61
- data/test/test_integration_script_6_tp_finish.rb +1 -0
- metadata +135 -81
data/lib/typingpool/project.rb
CHANGED
@@ -8,6 +8,8 @@ module Typingpool
|
|
8
8
|
#project is also associated with audio files on a remote server.
|
9
9
|
class Project
|
10
10
|
require 'uri'
|
11
|
+
require 'typingpool/project/local'
|
12
|
+
require 'typingpool/project/remote'
|
11
13
|
|
12
14
|
#Returns a time interval corresponding to the length of each audio
|
13
15
|
#chunk within the project. (Each chunk may be transcribed
|
@@ -193,401 +195,5 @@ module Typingpool
|
|
193
195
|
(1 - hms.count .. -1).each{|i| hms[i] = hms[i].to_s.rjust(2, '0') }
|
194
196
|
hms.join(":")
|
195
197
|
end
|
196
|
-
|
197
|
-
#Representation of the Project instance on remote servers. This is
|
198
|
-
#basically a collection of audio files to be transcribed and HTML
|
199
|
-
#files containing instructions and a form for the
|
200
|
-
#transcribers. The backend can be Amazon S3 (the default) or an
|
201
|
-
#SFTP server. Each backend is encapsulated in its own subclass. A
|
202
|
-
#backend subclass must provide a 'put' method, which takes an
|
203
|
-
#array of IO streams and an optional array of remote file
|
204
|
-
#basenames; a 'remove' method, which takes an array of remote file
|
205
|
-
#basenames; and the methods 'host' and 'path', which return the
|
206
|
-
#location of the destination server and destination directory,
|
207
|
-
#respectively.
|
208
|
-
#
|
209
|
-
#Thus, there will always be 'put', 'remove', 'host' and 'path'
|
210
|
-
#methods available, in addition to the Project::Remote methods
|
211
|
-
#outlined below.
|
212
|
-
class Remote
|
213
|
-
|
214
|
-
#The project name
|
215
|
-
attr_accessor :name
|
216
|
-
|
217
|
-
#Constructor. Takes the project name and a Config
|
218
|
-
#instance. Returns a Project::Remote::S3 or
|
219
|
-
#Project::Remote::SFTP instance, depending on the particulars of
|
220
|
-
#the Config. If there are sufficient config params to return
|
221
|
-
#EITHER an S3 or SFTP subclass, it will prefer the SFTP
|
222
|
-
#subclass.
|
223
|
-
def self.from_config(name, config)
|
224
|
-
if config.sftp
|
225
|
-
SFTP.new(name, config.sftp)
|
226
|
-
elsif config.amazon && config.amazon.bucket
|
227
|
-
S3.new(name, config.amazon)
|
228
|
-
else
|
229
|
-
raise Error, "No valid upload params found in config file (SFTP or Amazon info)"
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
#Like project.remote.remove, except it takes an array of URLs
|
234
|
-
#instead an array of remote basenames, saving you from having to
|
235
|
-
#manually extract basenames from the URL.
|
236
|
-
def remove_urls(urls)
|
237
|
-
basenames = urls.map{|url| url_basename(url) }
|
238
|
-
remove(basenames){|file| yield(file) if block_given? }
|
239
|
-
end
|
240
|
-
|
241
|
-
#Given a file path, returns the URL to the file path were it to
|
242
|
-
#be uploaded by this instance.
|
243
|
-
def file_to_url(file)
|
244
|
-
"#{@url}/#{URI.escape(file)}"
|
245
|
-
end
|
246
|
-
|
247
|
-
#Given an URL, returns the file portion of the path, given the
|
248
|
-
#configuration of this instance.
|
249
|
-
def url_basename(url)
|
250
|
-
basename = url.split("#{self.url}/")[1] or raise Error, "Could not find base url '#{self.url}' within longer url '#{url}'"
|
251
|
-
URI.unescape(basename)
|
252
|
-
end
|
253
|
-
|
254
|
-
#Subclass for storing remote files on Amazon Simple Storage
|
255
|
-
#Service (S3)
|
256
|
-
class S3 < Remote
|
257
|
-
require 'aws/s3'
|
258
|
-
|
259
|
-
#An Amazon Web Services "Access Key ID." Set from the
|
260
|
-
#Config#amazon value passed to Project::Remote::S3.new, but
|
261
|
-
#changeable.
|
262
|
-
attr_accessor :key
|
263
|
-
|
264
|
-
#An Amazon Web Services "Secret Access Key." Set from the
|
265
|
-
#Config#amazon value passed to Project::Remote::S3.new, but
|
266
|
-
#changeable.
|
267
|
-
attr_accessor :secret
|
268
|
-
|
269
|
-
#The S3 "bucket" where uploads will be stores. Set from the
|
270
|
-
#Config#amazon value passed to Project::Remote::S3.new, but
|
271
|
-
#changeable.
|
272
|
-
attr_accessor :bucket
|
273
|
-
|
274
|
-
#Returns the base URL, which is prepended to the remote
|
275
|
-
#files. This is either the 'url' attribute of the
|
276
|
-
#Config#amazon value passed to Project::Remote::S3.new or, if
|
277
|
-
#that attribute is not set, the value returned by
|
278
|
-
#'default_url' (e.g. "https://bucketname.s3.amazonaws.com").
|
279
|
-
attr_reader :url
|
280
|
-
|
281
|
-
#Constructor. Takes the project name and the result of calling
|
282
|
-
#the 'amazon' method on a Config instance (i.e. the amazon
|
283
|
-
#section of a Config file).
|
284
|
-
def initialize(name, amazon_config)
|
285
|
-
@name = name
|
286
|
-
@config = amazon_config
|
287
|
-
@key = @config.key or raise Error::File::Remote::S3, "Missing Amazon key in config"
|
288
|
-
@secret = @config.secret or raise Error::File::Remote::S3, "Missing Amazon secret in config"
|
289
|
-
@bucket = @config.bucket or raise Error::File::Remote::S3, "Missing Amazon bucket in config"
|
290
|
-
@url = @config.url || default_url
|
291
|
-
end
|
292
|
-
|
293
|
-
#The remote host (server) name, parsed from #url
|
294
|
-
def host
|
295
|
-
URI.parse(@url).host
|
296
|
-
end
|
297
|
-
|
298
|
-
#The remote path (directory), pased from #url
|
299
|
-
def path
|
300
|
-
URI.parse(@url).path
|
301
|
-
end
|
302
|
-
|
303
|
-
#Upload files/strings to S3, optionally changing the names in the process.
|
304
|
-
# ==== Params
|
305
|
-
#[io_streams] Enumerable collection of IO objects, like a File
|
306
|
-
# or StringIO instance.
|
307
|
-
#[as] Optional if the io_streams are File instances. Array of
|
308
|
-
# file basenames, used to name the destination
|
309
|
-
# files. Default is the basename of the Files
|
310
|
-
# passed in as io_streams.
|
311
|
-
# ==== Returns
|
312
|
-
#Array of URLs corresponding to the uploaded files.
|
313
|
-
def put(io_streams, as=io_streams.map{|file| File.basename(file)})
|
314
|
-
batch(io_streams) do |stream, i|
|
315
|
-
dest = as[i]
|
316
|
-
yield(stream, dest) if block_given?
|
317
|
-
begin
|
318
|
-
AWS::S3::S3Object.store(dest, stream, @bucket, :access => :public_read)
|
319
|
-
rescue AWS::S3::NoSuchBucket
|
320
|
-
make_bucket
|
321
|
-
retry
|
322
|
-
end
|
323
|
-
file_to_url(dest)
|
324
|
-
end #batch
|
325
|
-
end
|
326
|
-
|
327
|
-
#Delete objects from S3.
|
328
|
-
# ==== Params
|
329
|
-
#[files] Enumerable collection of file names. Should NOT
|
330
|
-
# include the bucket name (path).
|
331
|
-
# ==== Returns
|
332
|
-
#Array of booleans corresponding to whether the delete call
|
333
|
-
#succeeded.
|
334
|
-
def remove(files)
|
335
|
-
batch(files) do |file, i|
|
336
|
-
yield(file) if block_given?
|
337
|
-
AWS::S3::S3Object.delete(file, @bucket)
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
protected
|
342
|
-
|
343
|
-
def batch(io_streams)
|
344
|
-
results = []
|
345
|
-
io_streams.each_with_index do |stream, i|
|
346
|
-
connect if i == 0
|
347
|
-
begin
|
348
|
-
results.push(yield(stream, i))
|
349
|
-
rescue AWS::S3::S3Exception => e
|
350
|
-
if e.message.match(/AWS::S3::SignatureDoesNotMatch/)
|
351
|
-
raise Error::File::Remote::S3::Credentials, "S3 operation failed with a signature error. This likely means your AWS key or secret is wrong. Error: #{e}"
|
352
|
-
else
|
353
|
-
raise Error::File::Remote::S3, "Your S3 operation failed with an Amazon error: #{e}"
|
354
|
-
end #if
|
355
|
-
end #begin
|
356
|
-
end #files.each
|
357
|
-
disconnect unless io_streams.empty?
|
358
|
-
results
|
359
|
-
end
|
360
|
-
|
361
|
-
def connect
|
362
|
-
AWS::S3::Base.establish_connection!(
|
363
|
-
:access_key_id => @key,
|
364
|
-
:secret_access_key => @secret,
|
365
|
-
:persistent => true,
|
366
|
-
:use_ssl => true
|
367
|
-
)
|
368
|
-
end
|
369
|
-
|
370
|
-
def disconnect
|
371
|
-
AWS::S3::Base.disconnect
|
372
|
-
end
|
373
|
-
|
374
|
-
def make_bucket
|
375
|
-
AWS::S3::Bucket.create(@bucket)
|
376
|
-
end
|
377
|
-
|
378
|
-
def default_url
|
379
|
-
"https://#{@bucket}.s3.amazonaws.com"
|
380
|
-
end
|
381
|
-
end #S3
|
382
|
-
|
383
|
-
#Subclass for storing remote files on an SFTP server. Only
|
384
|
-
#public/private key authentication has been tested. There is not
|
385
|
-
#yet any provision for password-based authentication, though
|
386
|
-
#adding it should be trivial.
|
387
|
-
class SFTP < Remote
|
388
|
-
require 'net/sftp'
|
389
|
-
|
390
|
-
#Returns the remote host (server) name. This is set from
|
391
|
-
#Config#sftp#host.
|
392
|
-
attr_reader :host
|
393
|
-
|
394
|
-
#Returns the remote path (directory). This is set from
|
395
|
-
#Config#sftp#path.
|
396
|
-
attr_reader :path
|
397
|
-
|
398
|
-
#Returns the name of the user used to log in to the SFTP
|
399
|
-
#server. This is et from Config#sftp#user.
|
400
|
-
attr_reader :user
|
401
|
-
|
402
|
-
#Returns the base URL, which is prepended to the remote
|
403
|
-
#files. This is set from Config#sftp#url.
|
404
|
-
attr_reader :url
|
405
|
-
|
406
|
-
#Constructor. Takes the project name and a Config#sftp.
|
407
|
-
def initialize(name, sftp_config)
|
408
|
-
@name = name
|
409
|
-
@config = sftp_config
|
410
|
-
@user = @config.user or raise Error::File::Remote::SFTP, "No SFTP user specified in config"
|
411
|
-
@host = @config.host or raise Error::File::Remote::SFTP, "No SFTP host specified in config"
|
412
|
-
@url = @config.url or raise Error::File::Remote::SFTP, "No SFTP url specified in config"
|
413
|
-
@path = @config.path || ''
|
414
|
-
end
|
415
|
-
|
416
|
-
#See docs for Project::Remote::S3#put.
|
417
|
-
def put(io_streams, as=io_streams.map{|file| File.basename(file)})
|
418
|
-
begin
|
419
|
-
i = 0
|
420
|
-
batch(io_streams) do |stream, connection|
|
421
|
-
dest = as[i]
|
422
|
-
i += 1
|
423
|
-
yield(stream, dest) if block_given?
|
424
|
-
connection.upload(stream, join_with_path(dest))
|
425
|
-
file_to_url(dest)
|
426
|
-
end
|
427
|
-
rescue Net::SFTP::StatusException => e
|
428
|
-
raise Error::File::Remote::SFTP, "SFTP upload failed: #{e.description}"
|
429
|
-
end
|
430
|
-
end
|
431
|
-
|
432
|
-
#See docs for Project::Remote::S3#remove.
|
433
|
-
def remove(files)
|
434
|
-
requests = batch(files) do |file, connection|
|
435
|
-
yield(file) if block_given?
|
436
|
-
connection.remove(join_with_path(file))
|
437
|
-
end
|
438
|
-
failures = requests.reject{|request| request.response.ok?}
|
439
|
-
if not(failures.empty?)
|
440
|
-
summary = failures.map{|request| request.response.to_s}.join('; ')
|
441
|
-
raise Error::File::Remote::SFTP, "SFTP removal failed: #{summary}"
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
protected
|
446
|
-
|
447
|
-
def connection
|
448
|
-
begin
|
449
|
-
Net::SFTP.start(@host, @user) do |connection|
|
450
|
-
yield(connection)
|
451
|
-
connection.loop
|
452
|
-
end
|
453
|
-
rescue Net::SSH::AuthenticationFailed
|
454
|
-
raise Error::File::Remote::SFTP, "SFTP authentication failed: #{$?}"
|
455
|
-
end
|
456
|
-
end
|
457
|
-
|
458
|
-
def batch(files)
|
459
|
-
results = []
|
460
|
-
connection do |connection|
|
461
|
-
files.each do |file|
|
462
|
-
results.push(yield(file, connection))
|
463
|
-
end
|
464
|
-
end
|
465
|
-
return results
|
466
|
-
end
|
467
|
-
|
468
|
-
def join_with_path(file)
|
469
|
-
if @path
|
470
|
-
[@path, file].join('/')
|
471
|
-
else
|
472
|
-
file
|
473
|
-
end
|
474
|
-
end
|
475
|
-
|
476
|
-
end #SFTP
|
477
|
-
end #Remote
|
478
|
-
|
479
|
-
#Representation of the Project instance in the local
|
480
|
-
#filesystem. Subclass of Filer::Dir; see Filer::Dir docs for
|
481
|
-
#additional details.
|
482
|
-
#
|
483
|
-
#This is basically a local dir with various subdirs and files
|
484
|
-
#containing the canonical representation of the project, including
|
485
|
-
#data on remote resources, the project ID and subtitle, the audio files
|
486
|
-
#themselves, and, when complete, an HTML transcript of that audio,
|
487
|
-
#along with supporting CSS and Javascript files.
|
488
|
-
class Local < Filer::Dir
|
489
|
-
require 'fileutils'
|
490
|
-
require 'securerandom'
|
491
|
-
|
492
|
-
#Returns the dir path.
|
493
|
-
attr_reader :path
|
494
|
-
|
495
|
-
class << self
|
496
|
-
#Constructor. Creates a directory in the filesystem for the
|
497
|
-
#project.
|
498
|
-
#
|
499
|
-
# ==== Params
|
500
|
-
# [name] Name of the associated project.
|
501
|
-
# [base_dir] Path to the local directory into which the project
|
502
|
-
# dir should be placed.
|
503
|
-
# [template_dir] Path to the dir which will be used as a base
|
504
|
-
# template for new projects.
|
505
|
-
# ==== Returns
|
506
|
-
# Project::Local instance.
|
507
|
-
def create(name, base_dir, template_dir)
|
508
|
-
local = super(File.join(base_dir, name))
|
509
|
-
FileUtils.cp_r(File.join(template_dir, '.'), local)
|
510
|
-
local.create_id
|
511
|
-
local
|
512
|
-
end
|
513
|
-
|
514
|
-
#Takes the name of a project and a path. If there's a
|
515
|
-
#directory with a matching name in the given path whose file
|
516
|
-
#layout indicates it is a Project::Local instance (see 'ours?'
|
517
|
-
#docs), returns a corresponding Project::Local instance.
|
518
|
-
def named(string, path)
|
519
|
-
match = super
|
520
|
-
if match && ours?(match)
|
521
|
-
return match
|
522
|
-
end
|
523
|
-
return
|
524
|
-
end
|
525
|
-
|
526
|
-
#Takes a Filer::Dir instance. Returns true or false depending on whether
|
527
|
-
#the file layout inside the dir indicates it is a
|
528
|
-
#Project::Local instance.
|
529
|
-
def ours?(dir)
|
530
|
-
File.exists?(dir.subdir('audio')) && File.exists?(dir.subdir('audio', 'originals'))
|
531
|
-
end
|
532
|
-
|
533
|
-
#Takes the name of a project and returns true if it is a valid
|
534
|
-
#name for a directory in the local filesystem, false if not.
|
535
|
-
def valid_name?(name)
|
536
|
-
Utility.in_temp_dir do |dir|
|
537
|
-
begin
|
538
|
-
FileUtils.mkdir(File.join(dir, name))
|
539
|
-
rescue Errno::ENOENT
|
540
|
-
return false
|
541
|
-
end #begin
|
542
|
-
return File.exists?(File.join(dir, name))
|
543
|
-
end #Utility.in_temp_dir do...
|
544
|
-
end
|
545
|
-
|
546
|
-
#Takes one or more symbols. Adds corresponding getter/setter
|
547
|
-
#and delete method(s) to Project::Local, which read (getter)
|
548
|
-
#and write (setter) and delete corresponding text files in the
|
549
|
-
#data directory.
|
550
|
-
#
|
551
|
-
#So, for example, 'data_file_accessor :name' would allow you
|
552
|
-
#to later create the file 'data/foo.txt' in the project dir by
|
553
|
-
#calling 'project.local.name = "Foo"', read that same file via
|
554
|
-
#'project.local.name', and delete the file via
|
555
|
-
#'project.local.delete_name'
|
556
|
-
def data_file_accessor(*syms)
|
557
|
-
syms.each do |sym|
|
558
|
-
define_method(sym) do
|
559
|
-
file('data',"#{sym.to_s}.txt").read
|
560
|
-
end
|
561
|
-
define_method("#{sym.to_s}=".to_sym) do |value|
|
562
|
-
file('data',"#{sym.to_s}.txt").write(value)
|
563
|
-
end
|
564
|
-
define_method("delete_#{sym.to_s}".to_sym) do
|
565
|
-
if File.exists? file('data',"#{sym.to_s}.txt")
|
566
|
-
File.delete(file('data',"#{sym.to_s}.txt"))
|
567
|
-
end
|
568
|
-
end
|
569
|
-
end
|
570
|
-
end
|
571
|
-
end #class << self
|
572
|
-
|
573
|
-
#Calling 'subtitle' will read 'data/subtitle.txt'; calling
|
574
|
-
#'subtitle=' will write 'data/subtitle.txt'; calling
|
575
|
-
#'delete_subtitle' will delete 'data/subtitle.txt'.
|
576
|
-
data_file_accessor :subtitle
|
577
|
-
|
578
|
-
#Returns the ID of the project, as stored in 'data/id.txt'.
|
579
|
-
def id
|
580
|
-
file('data','id.txt').read
|
581
|
-
end
|
582
|
-
|
583
|
-
#Creates a file storing the canonical ID of the project in
|
584
|
-
#'data/id.txt'. Raises an exception if the file already exists.
|
585
|
-
def create_id
|
586
|
-
if id
|
587
|
-
raise Error, "id already exists"
|
588
|
-
end
|
589
|
-
file('data','id.txt').write(SecureRandom.hex(16))
|
590
|
-
end
|
591
|
-
end #Local
|
592
198
|
end #Project
|
593
199
|
end #Typingpool
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Typingpool
|
2
|
+
class Template
|
3
|
+
|
4
|
+
#A Template::Assignment works just like a regular template, except
|
5
|
+
#that within each transcript dir (Config#transcript and the
|
6
|
+
#built-in app template dir) we search within a subdir called
|
7
|
+
#'assignment' first, then, after all the 'assignment' subdirs have
|
8
|
+
#been search, we look in the original template dirs.
|
9
|
+
class Assignment < Template
|
10
|
+
def self.look_in_from_config(*args)
|
11
|
+
look_in = super(*args)
|
12
|
+
look_in.unshift(look_in.reject{|dir| dir.empty? }.map{|dir| File.join(dir, 'assignment') })
|
13
|
+
look_in.flatten
|
14
|
+
end
|
15
|
+
end #Assignment
|
16
|
+
end #Template
|
17
|
+
end #Typingpool
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Typingpool
|
2
|
+
class Template
|
3
|
+
|
4
|
+
#This subclass provides two utility methods to all templates:
|
5
|
+
#read, for including the text of another template, and render, for
|
6
|
+
#rendering another template. Read takes a relative path,
|
7
|
+
#documented below. Render is passed the same hash as the parent
|
8
|
+
#template, merged with an optional override hash, as documented
|
9
|
+
#below.
|
10
|
+
#
|
11
|
+
#This subclass also makes it easier to use a hash as the top-level
|
12
|
+
#variable namespace when rendering ERB templates.
|
13
|
+
class Env
|
14
|
+
|
15
|
+
#Construtor. Takes a hash to be passed to the template and a
|
16
|
+
#template (ERB).
|
17
|
+
def initialize(hash, template)
|
18
|
+
@hash = hash
|
19
|
+
@template = template
|
20
|
+
end
|
21
|
+
|
22
|
+
#Method passed into each template. Takes a relative path and
|
23
|
+
#returns the text of the file at that path.
|
24
|
+
#
|
25
|
+
#The relative path is resolved as in look_in above, with the
|
26
|
+
#following difference: the current directory and each parent
|
27
|
+
#directory of the active template is searched first, up to the
|
28
|
+
#root transcript directory (either Config#template, the built-in
|
29
|
+
#app template dir, or any dir that has been manually added to
|
30
|
+
#look_in).
|
31
|
+
def read(path)
|
32
|
+
@template.class.new(path, localized_look_in).read.strip
|
33
|
+
end
|
34
|
+
|
35
|
+
#Method passed into each template. Takes a reltive path and
|
36
|
+
#returns the *rendered* text of the ERB template at that
|
37
|
+
#path. Can also take an optional hash, which will be merged into
|
38
|
+
#the parent template's hash and passed to the included
|
39
|
+
#template. If the optional hash it not passed, the parent
|
40
|
+
#template's hash will be passed to the included template
|
41
|
+
#unmodified.
|
42
|
+
#
|
43
|
+
#The relative path is resolved as described in the docs for
|
44
|
+
#Template::Env#read.
|
45
|
+
def render(path, hash={})
|
46
|
+
@template.class.new(path, localized_look_in).render(@hash.merge(hash)).strip
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_binding
|
50
|
+
binding()
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def localized_look_in
|
56
|
+
look_in = []
|
57
|
+
path = @template.full_path
|
58
|
+
until @template.look_in.include? path = File.dirname(path)
|
59
|
+
look_in.push(path)
|
60
|
+
end
|
61
|
+
look_in.push(path, (@template.look_in - [path])).flatten
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(key, value=nil)
|
65
|
+
if value
|
66
|
+
key = key.to_s.sub(/=$/, '')
|
67
|
+
@hash[key.to_sym] = value
|
68
|
+
end
|
69
|
+
if @hash.has_key? key
|
70
|
+
@hash[key]
|
71
|
+
elsif @hash.has_key? key.to_s
|
72
|
+
@hash[key.to_s]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end #Env
|
76
|
+
end #Template
|
77
|
+
end #Typingpool
|
data/lib/typingpool/template.rb
CHANGED
@@ -84,92 +84,7 @@ module Typingpool
|
|
84
84
|
def extensions
|
85
85
|
['.html.erb', '.erb', '']
|
86
86
|
end
|
87
|
-
|
88
|
-
|
89
|
-
#A Template::Assignment works just like a regular template, except
|
90
|
-
#that within each transcript dir (Config#transcript and the
|
91
|
-
#built-in app template dir) we search within a subdir called
|
92
|
-
#'assignment' first, then, after all the 'assignment' subdirs have
|
93
|
-
#been search, we look in the original template dirs.
|
94
|
-
class Assignment < Template
|
95
|
-
def self.look_in_from_config(*args)
|
96
|
-
look_in = super(*args)
|
97
|
-
look_in.unshift(look_in.reject{|dir| dir.empty? }.map{|dir| File.join(dir, 'assignment') })
|
98
|
-
look_in.flatten
|
99
|
-
end
|
100
|
-
end #Assignment
|
101
|
-
|
102
|
-
#This subclass provides two utility methods to all templates:
|
103
|
-
#read, for including the text of another template, and render, for
|
104
|
-
#rendering another template. Read takes a relative path,
|
105
|
-
#documented below. Render is passed the same hash as the parent
|
106
|
-
#template, merged with an optional override hash, as documented
|
107
|
-
#below.
|
108
|
-
#
|
109
|
-
#This subclass also makes it easier to use a hash as the top-level
|
110
|
-
#variable namespace when rendering ERB templates.
|
111
|
-
class Env
|
112
|
-
|
113
|
-
#Construtor. Takes a hash to be passed to the template and a
|
114
|
-
#template (ERB).
|
115
|
-
def initialize(hash, template)
|
116
|
-
@hash = hash
|
117
|
-
@template = template
|
118
|
-
end
|
119
|
-
|
120
|
-
#Method passed into each template. Takes a relative path and
|
121
|
-
#returns the text of the file at that path.
|
122
|
-
#
|
123
|
-
#The relative path is resolved as in look_in above, with the
|
124
|
-
#following difference: the current directory and each parent
|
125
|
-
#directory of the active template is searched first, up to the
|
126
|
-
#root transcript directory (either Config#template, the built-in
|
127
|
-
#app template dir, or any dir that has been manually added to
|
128
|
-
#look_in).
|
129
|
-
def read(path)
|
130
|
-
@template.class.new(path, localized_look_in).read.strip
|
131
|
-
end
|
132
|
-
|
133
|
-
#Method passed into each template. Takes a reltive path and
|
134
|
-
#returns the *rendered* text of the ERB template at that
|
135
|
-
#path. Can also take an optional hash, which will be merged into
|
136
|
-
#the parent template's hash and passed to the included
|
137
|
-
#template. If the optional hash it not passed, the parent
|
138
|
-
#template's hash will be passed to the included template
|
139
|
-
#unmodified.
|
140
|
-
#
|
141
|
-
#The relative path is resolved as described in the docs for
|
142
|
-
#Template::Env#read.
|
143
|
-
def render(path, hash={})
|
144
|
-
@template.class.new(path, localized_look_in).render(@hash.merge(hash)).strip
|
145
|
-
end
|
146
|
-
|
147
|
-
def get_binding
|
148
|
-
binding()
|
149
|
-
end
|
150
|
-
|
151
|
-
protected
|
152
|
-
|
153
|
-
def localized_look_in
|
154
|
-
look_in = []
|
155
|
-
path = @template.full_path
|
156
|
-
until @template.look_in.include? path = File.dirname(path)
|
157
|
-
look_in.push(path)
|
158
|
-
end
|
159
|
-
look_in.push(path, (@template.look_in - [path])).flatten
|
160
|
-
end
|
161
|
-
|
162
|
-
def method_missing(key, value=nil)
|
163
|
-
if value
|
164
|
-
key = key.to_s.sub(/=$/, '')
|
165
|
-
@hash[key.to_sym] = value
|
166
|
-
end
|
167
|
-
if @hash.has_key? key
|
168
|
-
@hash[key]
|
169
|
-
elsif @hash.has_key? key.to_s
|
170
|
-
@hash[key.to_s]
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end #Env
|
87
|
+
require 'typingpool/template/assignment'
|
88
|
+
require 'typingpool/template/env'
|
174
89
|
end #Template
|
175
90
|
end #Typingpool
|