rivendell-import 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/db/migrate/20121004194900_add_delete_file_to_task.rb +9 -0
- data/db/migrate/2012100620361349548586_add_xport_options_to_task.rb +9 -0
- data/examples/config.rb +9 -0
- data/lib/rivendell/import.rb +2 -0
- data/lib/rivendell/import/application.rb +53 -0
- data/lib/rivendell/import/base.rb +6 -0
- data/lib/rivendell/import/cart.rb +9 -1
- data/lib/rivendell/import/cli.rb +41 -5
- data/lib/rivendell/import/config.rb +22 -4
- data/lib/rivendell/import/config_loader.rb +43 -0
- data/lib/rivendell/import/file.rb +5 -0
- data/lib/rivendell/import/notifier/base.rb +7 -2
- data/lib/rivendell/import/static/rivendell-import.png +0 -0
- data/lib/rivendell/import/static/screen.css +137 -0
- data/lib/rivendell/import/task.rb +25 -2
- data/lib/rivendell/import/tasking/file.rb +6 -0
- data/lib/rivendell/import/tasking/status.rb +22 -1
- data/lib/rivendell/import/tasks.rb +0 -4
- data/lib/rivendell/import/version.rb +1 -1
- data/lib/rivendell/import/views/index.erb +56 -0
- data/rivendell-import.gemspec +5 -1
- data/spec/rivendell/import/base_spec.rb +11 -0
- data/spec/rivendell/import/cart_spec.rb +33 -2
- data/spec/rivendell/import/cli_spec.rb +47 -3
- data/spec/rivendell/import/config_spec.rb +24 -2
- data/spec/rivendell/import/notifier/base_spec.rb +15 -1
- data/spec/rivendell/import/task_spec.rb +70 -4
- data/spec/rivendell/import/tasks_spec.rb +0 -13
- data/spec/rivendell/import/worker_spec.rb +1 -0
- data/spec/support/database_cleaner.rb +2 -1
- metadata +61 -6
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@ source "http://rubygems.org"
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
|
5
|
+
gem "rivendell-api" # , :git => "git://projects.tryphon.priv/rivendell-api", :ref => "b98a46fbb0c31d965853206a56500cea4a340044" # , :path => "~/Projects/RivendellApi"
|
6
6
|
|
7
7
|
if RUBY_PLATFORM =~ /linux/
|
8
8
|
gem 'libnotify'
|
data/examples/config.rb
CHANGED
@@ -15,17 +15,26 @@ end
|
|
15
15
|
Rivendell::Import::Notifier::Mail.from = "root@tryphon.eu"
|
16
16
|
|
17
17
|
Rivendell::Import.config do |config|
|
18
|
+
|
19
|
+
config.rivendell.host = "localhost"
|
20
|
+
config.rivendell.login_name = "user"
|
21
|
+
config.rivendell.password = ""
|
22
|
+
|
18
23
|
config.to_prepare do |file|
|
19
24
|
file.in("music") do
|
20
25
|
cart.group = "MUSIC"
|
21
26
|
end
|
22
27
|
|
23
28
|
file.in("pad") do
|
29
|
+
cart.clear_cuts!
|
24
30
|
cart.find_by_title file.basename
|
25
31
|
end
|
26
32
|
|
27
33
|
cart.group ||= "TEST"
|
28
34
|
|
35
|
+
# To delete file when task is completed
|
36
|
+
#task.delete_file!
|
37
|
+
|
29
38
|
notify 'alban@tryphon.eu', :by => :email
|
30
39
|
end
|
31
40
|
end
|
data/lib/rivendell/import.rb
CHANGED
@@ -34,6 +34,8 @@ ActiveRecord::Base.include_root_in_json = false
|
|
34
34
|
|
35
35
|
require "rivendell/api"
|
36
36
|
|
37
|
+
require "rivendell/import/config"
|
38
|
+
require "rivendell/import/config_loader"
|
37
39
|
require "rivendell/import/worker"
|
38
40
|
require "rivendell/import/task"
|
39
41
|
require "rivendell/import/tasks"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
require 'will_paginate'
|
4
|
+
require 'will_paginate/active_record'
|
5
|
+
|
6
|
+
module Rivendell::Import
|
7
|
+
class Application < Sinatra::Application
|
8
|
+
|
9
|
+
set :public_folder, ::File.expand_path('static', ::File.dirname(__FILE__))
|
10
|
+
# set :static_cache_control, [:public, :max_age => 3600]
|
11
|
+
|
12
|
+
get '/' do
|
13
|
+
redirect "/tasks", 302
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/tasks' do
|
17
|
+
erb :index, :locals => { :tasks => tasks.paginate(:page => params[:page], :per_page => 15) }
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/tasks.json' do
|
21
|
+
tasks.to_json
|
22
|
+
end
|
23
|
+
|
24
|
+
def tasks
|
25
|
+
Task.order("updated_at DESC")
|
26
|
+
end
|
27
|
+
|
28
|
+
helpers do
|
29
|
+
def distance_of_time_in_words_from_now(time)
|
30
|
+
distance = Time.now - time
|
31
|
+
|
32
|
+
if distance < 43200 # less than 12 hours
|
33
|
+
time.strftime("%H:%M")
|
34
|
+
else
|
35
|
+
time.strftime("%d/%m")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def truncate_filename(path, length)
|
40
|
+
path = path.to_s
|
41
|
+
|
42
|
+
if path.size < length
|
43
|
+
path
|
44
|
+
else
|
45
|
+
path[0..length] + "..."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -10,7 +10,13 @@ module Rivendell::Import
|
|
10
10
|
@workers = []
|
11
11
|
end
|
12
12
|
|
13
|
+
@@default_to_prepare = nil
|
14
|
+
cattr_accessor :default_to_prepare
|
15
|
+
|
13
16
|
attr_accessor :to_prepare
|
17
|
+
def to_prepare
|
18
|
+
@to_prepare or default_to_prepare
|
19
|
+
end
|
14
20
|
|
15
21
|
def listen(directory, options = {})
|
16
22
|
workers << Worker.new(self).start unless options[:dry_run]
|
@@ -5,7 +5,7 @@ module Rivendell::Import
|
|
5
5
|
include ActiveModel::Serializers::JSON
|
6
6
|
|
7
7
|
def attributes
|
8
|
-
%w{number group}.inject({}) do |map, attribute|
|
8
|
+
%w{number group clear_cuts?}.inject({}) do |map, attribute|
|
9
9
|
value = send attribute
|
10
10
|
map[attribute] = value if value
|
11
11
|
map
|
@@ -48,6 +48,7 @@ module Rivendell::Import
|
|
48
48
|
raise "File #{file.path} not found" unless file.exists?
|
49
49
|
|
50
50
|
cut.create
|
51
|
+
xport.clear_cuts number if clear_cuts?
|
51
52
|
xport.import number, cut.number, file.path
|
52
53
|
cut.update
|
53
54
|
end
|
@@ -58,6 +59,13 @@ module Rivendell::Import
|
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
62
|
+
attr_accessor :clear_cuts
|
63
|
+
alias_method :clear_cuts?, :clear_cuts
|
64
|
+
|
65
|
+
def clear_cuts!
|
66
|
+
self.clear_cuts = true
|
67
|
+
end
|
68
|
+
|
61
69
|
def carts_cache
|
62
70
|
@carts_cache ||= Rivendell::Import::CartsCache.new(xport)
|
63
71
|
end
|
data/lib/rivendell/import/cli.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'trollop'
|
2
2
|
require 'logger'
|
3
|
+
require 'syslog/logger'
|
3
4
|
|
4
5
|
module Rivendell::Import
|
5
6
|
class CLI
|
@@ -27,6 +28,10 @@ module Rivendell::Import
|
|
27
28
|
options[:debug]
|
28
29
|
end
|
29
30
|
|
31
|
+
def syslog?
|
32
|
+
options[:syslog]
|
33
|
+
end
|
34
|
+
|
30
35
|
def database
|
31
36
|
options[:database]
|
32
37
|
end
|
@@ -37,6 +42,7 @@ module Rivendell::Import
|
|
37
42
|
opt :listen, "Wait for files in given directory"
|
38
43
|
opt :dry_run, "Just create tasks without executing them"
|
39
44
|
opt :debug, "Enable debug messages (in stderr)"
|
45
|
+
opt :syslog, "Log messages to syslog"
|
40
46
|
opt :database, "The database file used to store tasks", :type => String
|
41
47
|
end
|
42
48
|
end
|
@@ -62,8 +68,39 @@ module Rivendell::Import
|
|
62
68
|
parsed_parser.leftovers
|
63
69
|
end
|
64
70
|
|
71
|
+
def setup_logger
|
72
|
+
new_logger =
|
73
|
+
if syslog?
|
74
|
+
Syslog::Logger.new('rivendell-import').tap do |syslog|
|
75
|
+
syslog.level = debug? ? Logger::DEBUG : Logger::INFO
|
76
|
+
end
|
77
|
+
elsif debug?
|
78
|
+
Logger.new($stderr)
|
79
|
+
end
|
80
|
+
|
81
|
+
Rivendell::Import.logger = new_logger if new_logger
|
82
|
+
end
|
83
|
+
|
84
|
+
def start_webserver
|
85
|
+
require 'rivendell/import/application'
|
86
|
+
|
87
|
+
Rivendell::Import.logger.debug "Start webserver"
|
88
|
+
|
89
|
+
Thread.new do
|
90
|
+
Rivendell::Import::Application.run!
|
91
|
+
|
92
|
+
# FIXME we don't see difference between normal quit and start error (EADDRINUSE, ...)
|
93
|
+
Rivendell::Import.logger.debug "Webserver is stopped"
|
94
|
+
exit 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def config_loader
|
99
|
+
ConfigLoader.new(config_file, listen_mode?)
|
100
|
+
end
|
101
|
+
|
65
102
|
def run
|
66
|
-
|
103
|
+
setup_logger
|
67
104
|
|
68
105
|
if database
|
69
106
|
Rivendell::Import.establish_connection database
|
@@ -71,12 +108,11 @@ module Rivendell::Import
|
|
71
108
|
Rivendell::Import.establish_connection
|
72
109
|
end
|
73
110
|
|
74
|
-
|
75
|
-
load config_file
|
76
|
-
import.to_prepare = Rivendell::Import.config.to_prepare
|
77
|
-
end
|
111
|
+
config_loader.load
|
78
112
|
|
79
113
|
if listen_mode?
|
114
|
+
start_webserver
|
115
|
+
|
80
116
|
listen_options = {}
|
81
117
|
listen_options[:dry_run] = true if dry_run?
|
82
118
|
|
@@ -2,11 +2,29 @@ module Rivendell::Import
|
|
2
2
|
class Config
|
3
3
|
|
4
4
|
def to_prepare(&block)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
Base.default_to_prepare = block
|
6
|
+
end
|
7
|
+
|
8
|
+
def rivendell
|
9
|
+
@rivendell ||= Rivendell.new
|
10
|
+
end
|
11
|
+
alias_method :xport_options, :rivendell
|
12
|
+
|
13
|
+
class Rivendell
|
14
|
+
|
15
|
+
def host=(host)
|
16
|
+
Task.default_xport_options[:host] = host
|
9
17
|
end
|
18
|
+
|
19
|
+
def login_name=(login_name)
|
20
|
+
Task.default_xport_options[:login_name] = login_name
|
21
|
+
end
|
22
|
+
alias_method :user=, :login_name=
|
23
|
+
|
24
|
+
def password=(password)
|
25
|
+
Task.default_xport_options[:password] = password
|
26
|
+
end
|
27
|
+
|
10
28
|
end
|
11
29
|
|
12
30
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rivendell::Import
|
2
|
+
class ConfigLoader
|
3
|
+
|
4
|
+
attr_accessor :file, :auto_reload
|
5
|
+
alias_method :auto_reload?, :auto_reload
|
6
|
+
|
7
|
+
def initialize(file, auto_reload = false)
|
8
|
+
self.file = file
|
9
|
+
self.auto_reload = auto_reload
|
10
|
+
|
11
|
+
listen_file if auto_reload?
|
12
|
+
end
|
13
|
+
|
14
|
+
def load
|
15
|
+
Kernel.load file
|
16
|
+
end
|
17
|
+
|
18
|
+
def listen_file
|
19
|
+
callback = Proc.new do |modified, added, removed|
|
20
|
+
if modified.include? absolute_path
|
21
|
+
Rivendell::Import.logger.info "Configuration changed, reload it"
|
22
|
+
load
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Rivendell::Import.logger.info "Listen to config file changes (#{file})"
|
27
|
+
Listen.to(directory).filter(/^#{basename}$/).change(&callback).start(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
def absolute_path
|
31
|
+
::File.expand_path(file)
|
32
|
+
end
|
33
|
+
|
34
|
+
def basename
|
35
|
+
::File.basename(file)
|
36
|
+
end
|
37
|
+
|
38
|
+
def directory
|
39
|
+
::File.dirname(file)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -58,10 +58,15 @@ module Rivendell::Import::Notifier
|
|
58
58
|
|
59
59
|
after_initialize :read_parameters
|
60
60
|
|
61
|
+
def parameters_hash
|
62
|
+
parameters_as_string = parameters.sort_by { |p| p.first.to_s }.flatten.join('-')
|
63
|
+
Digest::SHA256.hexdigest parameters_as_string
|
64
|
+
end
|
65
|
+
|
61
66
|
def write_parameters
|
62
67
|
if parameters.present?
|
63
68
|
write_attribute :parameters, parameters.to_json
|
64
|
-
write_attribute :key,
|
69
|
+
write_attribute :key, parameters_hash
|
65
70
|
else
|
66
71
|
write_attribute :parameters, nil
|
67
72
|
write_attribute :key, nil
|
@@ -76,7 +81,7 @@ module Rivendell::Import::Notifier
|
|
76
81
|
Mail.new options.merge(:to => target)
|
77
82
|
end
|
78
83
|
|
79
|
-
key = new_notifier.
|
84
|
+
key = new_notifier.parameters_hash
|
80
85
|
if existing_notifier = where(:type => new_notifier.type, :key => key).first
|
81
86
|
existing_notifier
|
82
87
|
else
|
Binary file
|
@@ -0,0 +1,137 @@
|
|
1
|
+
html {color:#000;}
|
2
|
+
body,div,ul,li,p,th,td{margin:0;padding:0;}
|
3
|
+
table {border-collapse:collapse;border-spacing:0;}
|
4
|
+
img {border:0;}
|
5
|
+
strong,b,i,em,th {font-style:normal;font-weight:normal;}
|
6
|
+
li {list-style:none;}
|
7
|
+
th {text-align:left;}
|
8
|
+
body {
|
9
|
+
margin: 0;
|
10
|
+
padding: 0;
|
11
|
+
font-size: 14px;
|
12
|
+
font-family: Trebuchet MS, Verdana, sans-serif;
|
13
|
+
}
|
14
|
+
a {
|
15
|
+
color: #5884a6;
|
16
|
+
text-decoration: none;
|
17
|
+
}
|
18
|
+
a:hover { text-decoration: underline; }
|
19
|
+
hr {
|
20
|
+
display: none;
|
21
|
+
clear: both;
|
22
|
+
}
|
23
|
+
img {
|
24
|
+
border: none;
|
25
|
+
vertical-align: middle;
|
26
|
+
}
|
27
|
+
strong { font-weight: bold; }
|
28
|
+
em { font-style: italic; }
|
29
|
+
#access { display: none; }
|
30
|
+
#header {
|
31
|
+
border-bottom: 4px solid #cccccc;
|
32
|
+
background: #333333;
|
33
|
+
margin-bottom: 20px;
|
34
|
+
}
|
35
|
+
#header div {
|
36
|
+
position: relative;
|
37
|
+
height: 100px;
|
38
|
+
width: 750px;
|
39
|
+
margin: 0 auto;
|
40
|
+
background: url("rivendell-import.png") no-repeat 0px 30px;
|
41
|
+
}
|
42
|
+
#menu {
|
43
|
+
position: absolute;
|
44
|
+
bottom: 0;
|
45
|
+
right: 0;
|
46
|
+
}
|
47
|
+
#menu li {
|
48
|
+
float: left;
|
49
|
+
margin: 0 0 0 8px;
|
50
|
+
background-color: #7196c4;
|
51
|
+
}
|
52
|
+
#menu li a {
|
53
|
+
display: block;
|
54
|
+
padding: 6px 10px;
|
55
|
+
font-size: 105%;
|
56
|
+
color: #ffffff;
|
57
|
+
}
|
58
|
+
#menu li a:hover {
|
59
|
+
color: #333333;
|
60
|
+
text-decoration: none;
|
61
|
+
}
|
62
|
+
#frame {
|
63
|
+
width: 750px;
|
64
|
+
margin: 0 auto;
|
65
|
+
}
|
66
|
+
#frame:after {
|
67
|
+
content: ".";
|
68
|
+
display: block;
|
69
|
+
height: 0;
|
70
|
+
clear: both;
|
71
|
+
visibility: hidden;
|
72
|
+
}
|
73
|
+
#content {
|
74
|
+
padding-top: 5px;
|
75
|
+
}
|
76
|
+
#content .empty {
|
77
|
+
margin-top: 10px;
|
78
|
+
font-style: italic;
|
79
|
+
}
|
80
|
+
#content .pagination {
|
81
|
+
clear: both;
|
82
|
+
padding: 4px 0;
|
83
|
+
font-size: 90%;
|
84
|
+
text-align: right;
|
85
|
+
}
|
86
|
+
#content .pagination .disabled { display: none; }
|
87
|
+
#flash div {
|
88
|
+
margin: 6px 0;
|
89
|
+
padding: 12px 0;
|
90
|
+
font-size: 115%;
|
91
|
+
font-weight: bold;
|
92
|
+
text-indent: 54px;
|
93
|
+
border-top: 1px solid #979797;
|
94
|
+
border-bottom: 1px solid #979797;
|
95
|
+
background: #e7e7e7 url("../images/ui/notice.png") no-repeat 28px;
|
96
|
+
}
|
97
|
+
#flash .success {
|
98
|
+
color: #1e7a1b;
|
99
|
+
border-color: #4bb426;
|
100
|
+
background: #b7e1af url("../images/ui/success.png") no-repeat 28px;
|
101
|
+
}
|
102
|
+
#flash .failure {
|
103
|
+
color: #c01100;
|
104
|
+
border-color: #ca4f35;
|
105
|
+
background: #ffbdbd url("../images/ui/failure.png") no-repeat 28px;
|
106
|
+
}
|
107
|
+
#flash .warning {
|
108
|
+
color: #c67109;
|
109
|
+
border-color: #c67109;
|
110
|
+
background: #f8e694 url("../images/ui/warning.png") no-repeat 28px;
|
111
|
+
}
|
112
|
+
#footer {
|
113
|
+
display: none;
|
114
|
+
clear: both;
|
115
|
+
color: #333333;
|
116
|
+
line-height: 18px;
|
117
|
+
margin: 0;
|
118
|
+
padding: 24px 0;
|
119
|
+
background: #e7db9f;
|
120
|
+
}
|
121
|
+
#footer div {
|
122
|
+
width: 760px;
|
123
|
+
margin: 0 auto;
|
124
|
+
text-align: left;
|
125
|
+
}
|
126
|
+
#content table {
|
127
|
+
width: 100%;
|
128
|
+
}
|
129
|
+
#content table th {
|
130
|
+
font-weight: bold;
|
131
|
+
}
|
132
|
+
#content table td, #content table th {
|
133
|
+
padding: 5px 2px;
|
134
|
+
}
|
135
|
+
#content table tr.odd {
|
136
|
+
background-color: #EAEAEA;
|
137
|
+
}
|
@@ -24,8 +24,24 @@ module Rivendell::Import
|
|
24
24
|
status.completed? or status.failed?
|
25
25
|
end
|
26
26
|
|
27
|
+
@@default_xport_options = {}
|
28
|
+
cattr_accessor :default_xport_options
|
29
|
+
|
30
|
+
def raw_xport_options
|
31
|
+
read_attribute :xport_options
|
32
|
+
end
|
33
|
+
|
34
|
+
def xport_options
|
35
|
+
@xport_options ||= (raw_xport_options ? JSON.parse(raw_xport_options).with_indifferent_access : default_xport_options.dup)
|
36
|
+
end
|
37
|
+
|
38
|
+
def write_xport_options
|
39
|
+
write_attribute :xport_options, (xport_options.present? ? xport_options.to_json : nil)
|
40
|
+
end
|
41
|
+
before_save :write_xport_options
|
42
|
+
|
27
43
|
def xport
|
28
|
-
@xport ||= Rivendell::API::Xport.new
|
44
|
+
@xport ||= Rivendell::API::Xport.new(xport_options)
|
29
45
|
end
|
30
46
|
|
31
47
|
def prepare(&block)
|
@@ -53,6 +69,11 @@ module Rivendell::Import
|
|
53
69
|
notifier.notify
|
54
70
|
end
|
55
71
|
end
|
72
|
+
after_status_changed :notify!, :on => [:completed, :failed]
|
73
|
+
|
74
|
+
def destroy_file!
|
75
|
+
file.destroy! if delete_file?
|
76
|
+
end
|
56
77
|
|
57
78
|
def run
|
58
79
|
logger.debug "Run #{self.inspect}"
|
@@ -64,16 +85,18 @@ module Rivendell::Import
|
|
64
85
|
cart.import file
|
65
86
|
cart.update
|
66
87
|
|
67
|
-
|
88
|
+
destroy_file!
|
68
89
|
change_status! :completed
|
69
90
|
|
70
91
|
logger.info "Imported Cart #{cart.number}"
|
71
92
|
rescue Exception => e
|
72
93
|
logger.error "Task failed : #{e}"
|
94
|
+
logger.debug e.backtrace.join("\n")
|
73
95
|
ensure
|
74
96
|
unless status.completed?
|
75
97
|
change_status! :failed
|
76
98
|
end
|
99
|
+
save!
|
77
100
|
end
|
78
101
|
|
79
102
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module Rivendell::Import::Tasking
|
2
2
|
module File
|
3
|
+
|
3
4
|
def file=(file)
|
4
5
|
if file
|
5
6
|
@file = file
|
@@ -16,5 +17,10 @@ module Rivendell::Import::Tasking
|
|
16
17
|
def file
|
17
18
|
@file ||= Rivendell::Import::File.new(file_name, :path => file_path)
|
18
19
|
end
|
20
|
+
|
21
|
+
def delete_file!
|
22
|
+
self.delete_file = true
|
23
|
+
end
|
24
|
+
|
19
25
|
end
|
20
26
|
end
|
@@ -9,6 +9,7 @@ module Rivendell::Import::Tasking
|
|
9
9
|
base.class_eval do
|
10
10
|
after_initialize :define_default_status
|
11
11
|
end
|
12
|
+
base.extend ClassMethods
|
12
13
|
end
|
13
14
|
|
14
15
|
def raw_status
|
@@ -22,7 +23,27 @@ module Rivendell::Import::Tasking
|
|
22
23
|
def change_status!(status)
|
23
24
|
logger.debug "Change status to #{status}"
|
24
25
|
update_attribute :status, status.to_s
|
25
|
-
notify! if ran?
|
26
|
+
# notify! if ran?
|
27
|
+
invoke_status_changed_callbacks
|
28
|
+
end
|
29
|
+
|
30
|
+
def invoke_status_changed_callbacks
|
31
|
+
callbacks = self.class.status_changed_callbacks.values_at(:all, status.to_sym).flatten
|
32
|
+
callbacks.each { |method| send method }
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
|
37
|
+
def status_changed_callbacks
|
38
|
+
@status_changed_callbacks ||= Hash.new { |h,k| h[k] = [] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_status_changed(method, options = {})
|
42
|
+
Array(options[:on] || :all).each do |status|
|
43
|
+
status_changed_callbacks[status] << method
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
26
47
|
end
|
27
48
|
|
28
49
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr">
|
3
|
+
<head>
|
4
|
+
<title>Rivendell Import</title>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
6
|
+
<link rel="shortcut icon" href="/favicon.ico" />
|
7
|
+
<link href="/screen.css" media="screen" rel="stylesheet" type="text/css" />
|
8
|
+
</head>
|
9
|
+
<body class="tasks index">
|
10
|
+
<ul id="access">
|
11
|
+
<li><a href="#content">Aller au contenu</a></li>
|
12
|
+
<li><a href="#sidebar">Aller au menu</a></li>
|
13
|
+
</ul>
|
14
|
+
<hr/>
|
15
|
+
<div id="header">
|
16
|
+
<div>
|
17
|
+
<ul id="menu">
|
18
|
+
<li><a href="http://wiki.tryphon.eu/rivendell-import/">Help</a></li>
|
19
|
+
</ul>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<hr/>
|
23
|
+
<div id="frame">
|
24
|
+
<div id="content">
|
25
|
+
<%= will_paginate tasks %>
|
26
|
+
|
27
|
+
<table>
|
28
|
+
<tr>
|
29
|
+
<th>#</th>
|
30
|
+
<th>Status</th>
|
31
|
+
<th>Since</th>
|
32
|
+
<th>File</th>
|
33
|
+
<th>Destination</th>
|
34
|
+
<th>Priority</th>
|
35
|
+
<tr/>
|
36
|
+
<% tasks.each_with_index do |task, index| %>
|
37
|
+
<tr class="<%= 'odd' if index.odd? %>">
|
38
|
+
<td><%= task.id %></td>
|
39
|
+
<td><%= task.status %></td>
|
40
|
+
<td><%= distance_of_time_in_words_from_now(task.updated_at) %></td>
|
41
|
+
<td><%= truncate_filename(task.file, 60) %></td>
|
42
|
+
<td><%= task.destination %></td>
|
43
|
+
<td>default</td>
|
44
|
+
</tr>
|
45
|
+
<% end %>
|
46
|
+
</table>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
<hr/>
|
50
|
+
<div id="footer">
|
51
|
+
<div>
|
52
|
+
|
53
|
+
</div>
|
54
|
+
</div>
|
55
|
+
</body>
|
56
|
+
</html>
|
data/rivendell-import.gemspec
CHANGED
@@ -17,12 +17,16 @@ Gem::Specification.new do |gem|
|
|
17
17
|
|
18
18
|
gem.add_runtime_dependency 'listen'
|
19
19
|
gem.add_runtime_dependency 'httmultiparty'
|
20
|
-
gem.add_runtime_dependency 'rivendell-api', '~> 0.0.
|
20
|
+
gem.add_runtime_dependency 'rivendell-api', '~> 0.0.3'
|
21
21
|
gem.add_runtime_dependency 'trollop'
|
22
22
|
gem.add_runtime_dependency 'activerecord', '~> 3.2.8'
|
23
23
|
gem.add_runtime_dependency 'activesupport', '~> 3.2.8'
|
24
24
|
gem.add_runtime_dependency 'mail'
|
25
25
|
gem.add_runtime_dependency 'sqlite3'
|
26
|
+
gem.add_runtime_dependency 'SyslogLogger', '~> 2.0'
|
27
|
+
|
28
|
+
gem.add_runtime_dependency 'sinatra'
|
29
|
+
gem.add_runtime_dependency 'will_paginate', '~> 3.0.0'
|
26
30
|
|
27
31
|
gem.add_development_dependency "simplecov"
|
28
32
|
gem.add_development_dependency "rspec"
|
@@ -14,6 +14,17 @@ describe Rivendell::Import::Base do
|
|
14
14
|
|
15
15
|
end
|
16
16
|
|
17
|
+
describe "#to_prepare" do
|
18
|
+
|
19
|
+
let(:block) { mock }
|
20
|
+
|
21
|
+
it "should use default_to_prepare if not defined" do
|
22
|
+
subject.stub :default_to_prepare => block
|
23
|
+
subject.to_prepare.should == block
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
17
28
|
describe "#create_task" do
|
18
29
|
|
19
30
|
let(:file) { Rivendell::Import::File.new "dummy.wav" }
|
@@ -83,8 +83,7 @@ describe Rivendell::Import::Cart do
|
|
83
83
|
|
84
84
|
before(:each) do
|
85
85
|
subject.number = 123
|
86
|
-
subject.stub :
|
87
|
-
subject.cut.stub :create => true, :number => 1, :update => true
|
86
|
+
subject.stub :cut => mock.as_null_object, :xport => mock.as_null_object
|
88
87
|
end
|
89
88
|
|
90
89
|
it "should create Cut" do
|
@@ -102,6 +101,25 @@ describe Rivendell::Import::Cart do
|
|
102
101
|
subject.import file
|
103
102
|
end
|
104
103
|
|
104
|
+
context "clear_cuts has been defined" do
|
105
|
+
before do
|
106
|
+
subject.clear_cuts!
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should invoke Xport#clear_cuts before create a new cut" do
|
110
|
+
subject.xport.should_receive(:clear_cuts).ordered.with(subject.number)
|
111
|
+
subject.xport.should_receive(:import).ordered
|
112
|
+
subject.import file
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "clear_cuts hasn't been defined" do
|
117
|
+
it "should not invoke Xport#clear_cuts" do
|
118
|
+
subject.xport.should_not_receive(:create_cuts).with(subject.number)
|
119
|
+
subject.import file
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
105
123
|
end
|
106
124
|
|
107
125
|
describe "#find_by_title" do
|
@@ -129,4 +147,17 @@ describe Rivendell::Import::Cart do
|
|
129
147
|
|
130
148
|
end
|
131
149
|
|
150
|
+
describe "#clear_cuts!" do
|
151
|
+
|
152
|
+
before do
|
153
|
+
subject.number = 123
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should set flag clear_cuts" do
|
157
|
+
subject.clear_cuts!
|
158
|
+
subject.clear_cuts.should be_true
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
132
163
|
end
|
@@ -40,6 +40,15 @@ describe Rivendell::Import::CLI do
|
|
40
40
|
|
41
41
|
end
|
42
42
|
|
43
|
+
describe "syslog?" do
|
44
|
+
|
45
|
+
it "should return true when --syslog is specified" do
|
46
|
+
subject.arguments << "--syslog"
|
47
|
+
subject.should be_syslog
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
43
52
|
describe "#import" do
|
44
53
|
|
45
54
|
it "should return a Rivendell::Import::Base instance" do
|
@@ -71,13 +80,18 @@ describe Rivendell::Import::CLI do
|
|
71
80
|
|
72
81
|
before(:each) do
|
73
82
|
subject.stub :paths => %w{file1 file2}
|
83
|
+
subject.stub :start_webserver
|
84
|
+
subject.stub :config_loader => mock(:load => true)
|
74
85
|
subject.import.tasks.stub :run => true
|
75
86
|
end
|
76
87
|
|
77
|
-
it "should
|
78
|
-
subject.
|
79
|
-
subject.
|
88
|
+
it "should setup logger" do
|
89
|
+
subject.should_receive(:setup_logger)
|
90
|
+
subject.run
|
91
|
+
end
|
80
92
|
|
93
|
+
it "should load config_file" do
|
94
|
+
subject.config_loader.should_receive(:load)
|
81
95
|
subject.run
|
82
96
|
end
|
83
97
|
|
@@ -146,4 +160,34 @@ describe Rivendell::Import::CLI do
|
|
146
160
|
|
147
161
|
end
|
148
162
|
|
163
|
+
describe "#setup_logger" do
|
164
|
+
|
165
|
+
context " with syslog option" do
|
166
|
+
|
167
|
+
before do
|
168
|
+
subject.stub :syslog? => true
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should use a SyslogLogger" do
|
172
|
+
Rivendell::Import.should_receive(:logger=).with(kind_of(Syslog::Logger))
|
173
|
+
subject.setup_logger
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
describe "#config_loader" do
|
181
|
+
|
182
|
+
it "should use config_file" do
|
183
|
+
subject.config_loader.file.should == subject.config_file
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should be auto_reload if listen mode is enabled" do
|
187
|
+
subject.stub :listen_mode? => true, :config_file => "dummy"
|
188
|
+
subject.config_loader.should be_auto_reload
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
149
193
|
end
|
@@ -1,16 +1,38 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Rivendell::Import::Config do
|
4
|
+
|
5
|
+
let(:config) { Rivendell::Import::Config.new }
|
4
6
|
|
5
7
|
describe "#to_prepare" do
|
6
8
|
|
7
9
|
let(:user_block) { Proc.new {} }
|
8
10
|
|
9
|
-
it "should define
|
11
|
+
it "should define Base.default_to_prepare with given block" do
|
10
12
|
subject.to_prepare(&user_block)
|
11
|
-
|
13
|
+
Rivendell::Import::Base.default_to_prepare.should == user_block
|
12
14
|
end
|
13
15
|
|
14
16
|
end
|
15
17
|
|
18
|
+
describe "#rivendell" do
|
19
|
+
|
20
|
+
subject { config.rivendell }
|
21
|
+
|
22
|
+
def self.it_should_define_task_default_xport_option(attribute)
|
23
|
+
describe "#{attribute}=" do
|
24
|
+
attribute = attribute.to_sym
|
25
|
+
it "should define Task.default_xport_options[:#{attribute}]" do
|
26
|
+
subject.send "#{attribute}=", "dummy"
|
27
|
+
Rivendell::Import::Task.default_xport_options[attribute].should == "dummy"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it_should_define_task_default_xport_option :host
|
33
|
+
it_should_define_task_default_xport_option :login_name
|
34
|
+
it_should_define_task_default_xport_option :password
|
35
|
+
|
36
|
+
end
|
37
|
+
|
16
38
|
end
|
@@ -55,9 +55,23 @@ describe Rivendell::Import::Notifier::Base do
|
|
55
55
|
subject.key = nil
|
56
56
|
subject.stub :parameters => { :dummy => true }
|
57
57
|
subject.save!
|
58
|
-
subject.key.should == subject.
|
58
|
+
subject.key.should == subject.parameters_hash
|
59
59
|
end
|
60
60
|
|
61
61
|
end
|
62
62
|
|
63
|
+
describe "#parameters_hash" do
|
64
|
+
|
65
|
+
let(:other) { Rivendell::Import::Notifier::Test.new }
|
66
|
+
|
67
|
+
it "should be identical when parameters are identical" do
|
68
|
+
subject.stub :parameters => { :first => 1, :second => 2 }
|
69
|
+
other.stub :parameters => { :second => 2, :first => 1 }
|
70
|
+
|
71
|
+
subject.parameters_hash.should == other.parameters_hash
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
end
|
76
|
+
|
63
77
|
end
|
@@ -27,6 +27,26 @@ describe Rivendell::Import::Task do
|
|
27
27
|
subject.xport.should be_instance_of(Rivendell::API::Xport)
|
28
28
|
end
|
29
29
|
|
30
|
+
it "should use xport_options" do
|
31
|
+
subject.xport_options[:host] = "dummy"
|
32
|
+
subject.xport.host.should == "dummy"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#xport_options" do
|
38
|
+
|
39
|
+
it "should use #default_xport_options" do
|
40
|
+
subject.stub :default_xport_options => { :host => "dummy" }
|
41
|
+
subject.xport_options.should == { :host => "dummy" }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should not modified #default_xport_options" do
|
45
|
+
subject.stub :default_xport_options => { :host => "dummy" }
|
46
|
+
subject.xport_options[:host] = "other"
|
47
|
+
subject.default_xport_options.should == { :host => "dummy" }
|
48
|
+
end
|
49
|
+
|
30
50
|
end
|
31
51
|
|
32
52
|
describe "#prepare" do
|
@@ -119,16 +139,29 @@ describe Rivendell::Import::Task do
|
|
119
139
|
subject.destination.should == Rivendell::Import::Task.find(subject).destination
|
120
140
|
end
|
121
141
|
|
142
|
+
def reloaded_task
|
143
|
+
subject.save
|
144
|
+
Rivendell::Import::Task.find(subject)
|
145
|
+
end
|
146
|
+
|
122
147
|
it "should store tags separated with commas" do
|
123
148
|
subject.tags << "tag1" << "tag2"
|
124
|
-
|
125
|
-
Rivendell::Import::Task.find(subject).raw_tags.should == "tag1,tag2"
|
149
|
+
reloaded_task.raw_tags.should == "tag1,tag2"
|
126
150
|
end
|
127
151
|
|
128
152
|
it "should store cart" do
|
129
153
|
subject.cart.number = 123
|
130
|
-
|
131
|
-
|
154
|
+
reloaded_task.cart.number.should == 123
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should store cart" do
|
158
|
+
subject.cart.number = 123
|
159
|
+
reloaded_task.cart.number.should == 123
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should store cart" do
|
163
|
+
subject.xport_options[:host] = "dummy"
|
164
|
+
reloaded_task.xport_options[:host].should == "dummy"
|
132
165
|
end
|
133
166
|
|
134
167
|
end
|
@@ -214,4 +247,37 @@ describe Rivendell::Import::Task do
|
|
214
247
|
|
215
248
|
end
|
216
249
|
|
250
|
+
describe "#delete_file!" do
|
251
|
+
|
252
|
+
it "should set flag delete_file" do
|
253
|
+
subject.delete_file!
|
254
|
+
subject.delete_file.should be_true
|
255
|
+
end
|
256
|
+
|
257
|
+
context "defined" do
|
258
|
+
|
259
|
+
before do
|
260
|
+
subject.stub :cart => mock.as_null_object
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
it "should destroy! file when task is completed" do
|
265
|
+
subject.delete_file!
|
266
|
+
subject.file.should_receive(:destroy!)
|
267
|
+
subject.run
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
context "not defined" do
|
273
|
+
|
274
|
+
it "should destroy! file when task is completed" do
|
275
|
+
subject.file.should_not_receive(:destroy!)
|
276
|
+
subject.run
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
217
283
|
end
|
@@ -4,19 +4,6 @@ describe Rivendell::Import::Tasks do
|
|
4
4
|
|
5
5
|
let(:file) { Rivendell::Import::File.new "dummy.wav" }
|
6
6
|
|
7
|
-
describe "#pending?" do
|
8
|
-
|
9
|
-
it "should be true when a task is pending" do
|
10
|
-
subject.create file
|
11
|
-
subject.should be_pending
|
12
|
-
end
|
13
|
-
|
14
|
-
it "should be false when queue is empty?" do
|
15
|
-
subject.should_not be_pending
|
16
|
-
end
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
7
|
describe "#run" do
|
21
8
|
|
22
9
|
it "should run each task" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rivendell-import
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: listen
|
@@ -50,7 +50,7 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - ~>
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 0.0.
|
53
|
+
version: 0.0.3
|
54
54
|
type: :runtime
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -58,7 +58,7 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ~>
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.0.
|
61
|
+
version: 0.0.3
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
63
|
name: trollop
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,6 +139,54 @@ dependencies:
|
|
139
139
|
- - ! '>='
|
140
140
|
- !ruby/object:Gem::Version
|
141
141
|
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: SyslogLogger
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ~>
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '2.0'
|
150
|
+
type: :runtime
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ~>
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '2.0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: sinatra
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :runtime
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: will_paginate
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ~>
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: 3.0.0
|
182
|
+
type: :runtime
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ~>
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: 3.0.0
|
142
190
|
- !ruby/object:Gem::Dependency
|
143
191
|
name: simplecov
|
144
192
|
requirement: !ruby/object:Gem::Requirement
|
@@ -285,6 +333,8 @@ files:
|
|
285
333
|
- db/migrate/20120920712200_create_tasks.rb
|
286
334
|
- db/migrate/20120927194300_create_notifiers.rb
|
287
335
|
- db/migrate/20120927203600_create_notifications.rb
|
336
|
+
- db/migrate/20121004194900_add_delete_file_to_task.rb
|
337
|
+
- db/migrate/2012100620361349548586_add_xport_options_to_task.rb
|
288
338
|
- examples/.gitignore
|
289
339
|
- examples/config.rb
|
290
340
|
- features/manage_cart_attributes.feature
|
@@ -293,11 +343,13 @@ files:
|
|
293
343
|
- features/support/env.rb
|
294
344
|
- features/support/mock_xport.rb
|
295
345
|
- lib/rivendell/import.rb
|
346
|
+
- lib/rivendell/import/application.rb
|
296
347
|
- lib/rivendell/import/base.rb
|
297
348
|
- lib/rivendell/import/cart.rb
|
298
349
|
- lib/rivendell/import/carts_cache.rb
|
299
350
|
- lib/rivendell/import/cli.rb
|
300
351
|
- lib/rivendell/import/config.rb
|
352
|
+
- lib/rivendell/import/config_loader.rb
|
301
353
|
- lib/rivendell/import/context.rb
|
302
354
|
- lib/rivendell/import/cut.rb
|
303
355
|
- lib/rivendell/import/file.rb
|
@@ -307,6 +359,8 @@ files:
|
|
307
359
|
- lib/rivendell/import/notifier/mail-subject.erb
|
308
360
|
- lib/rivendell/import/notifier/mail.rb
|
309
361
|
- lib/rivendell/import/notifiers.rb
|
362
|
+
- lib/rivendell/import/static/rivendell-import.png
|
363
|
+
- lib/rivendell/import/static/screen.css
|
310
364
|
- lib/rivendell/import/task.rb
|
311
365
|
- lib/rivendell/import/tasking/cart.rb
|
312
366
|
- lib/rivendell/import/tasking/destination.rb
|
@@ -315,6 +369,7 @@ files:
|
|
315
369
|
- lib/rivendell/import/tasking/tags.rb
|
316
370
|
- lib/rivendell/import/tasks.rb
|
317
371
|
- lib/rivendell/import/version.rb
|
372
|
+
- lib/rivendell/import/views/index.erb
|
318
373
|
- lib/rivendell/import/worker.rb
|
319
374
|
- log/.gitkeep
|
320
375
|
- rivendell-import.gemspec
|
@@ -356,7 +411,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
356
411
|
version: '0'
|
357
412
|
segments:
|
358
413
|
- 0
|
359
|
-
hash:
|
414
|
+
hash: 900421457418897316
|
360
415
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
361
416
|
none: false
|
362
417
|
requirements:
|
@@ -365,7 +420,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
365
420
|
version: '0'
|
366
421
|
segments:
|
367
422
|
- 0
|
368
|
-
hash:
|
423
|
+
hash: 900421457418897316
|
369
424
|
requirements: []
|
370
425
|
rubyforge_project:
|
371
426
|
rubygems_version: 1.8.23
|