coupler 0.0.7-java → 0.0.8-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/VERSION +1 -1
  2. data/coupler.gemspec +11 -2
  3. data/db/migrate/024_add_error_msg_to_jobs.rb +5 -0
  4. data/db/migrate/025_add_notifications.rb +16 -0
  5. data/db/migrate/026_add_status_to_resources.rb +7 -0
  6. data/db/migrate/027_add_notification_id_to_jobs.rb +8 -0
  7. data/lib/coupler.rb +6 -2
  8. data/lib/coupler/base.rb +1 -0
  9. data/lib/coupler/extensions.rb +3 -1
  10. data/lib/coupler/extensions/imports.rb +11 -14
  11. data/lib/coupler/extensions/notifications.rb +26 -0
  12. data/lib/coupler/models.rb +1 -0
  13. data/lib/coupler/models/comparison.rb +0 -1
  14. data/lib/coupler/models/connection.rb +0 -1
  15. data/lib/coupler/models/field.rb +6 -6
  16. data/lib/coupler/models/import.rb +9 -3
  17. data/lib/coupler/models/job.rb +64 -20
  18. data/lib/coupler/models/matcher.rb +0 -1
  19. data/lib/coupler/models/notification.rb +7 -0
  20. data/lib/coupler/models/project.rb +0 -1
  21. data/lib/coupler/models/resource.rb +52 -31
  22. data/lib/coupler/models/result.rb +0 -1
  23. data/lib/coupler/models/scenario.rb +0 -1
  24. data/lib/coupler/models/transformation.rb +2 -3
  25. data/lib/coupler/models/transformer.rb +0 -1
  26. data/lib/coupler/scheduler.rb +8 -0
  27. data/tasks/db.rake +2 -2
  28. data/test/functional/test_imports.rb +21 -13
  29. data/test/functional/test_notifications.rb +38 -0
  30. data/test/integration/test_import.rb +25 -1
  31. data/test/unit/models/test_field.rb +2 -13
  32. data/test/unit/models/test_import.rb +4 -0
  33. data/test/unit/models/test_job.rb +59 -6
  34. data/test/unit/models/test_notification.rb +17 -0
  35. data/test/unit/models/test_resource.rb +114 -29
  36. data/test/unit/models/test_transformation.rb +4 -4
  37. data/test/unit/test_base.rb +1 -1
  38. data/test/unit/test_scheduler.rb +11 -0
  39. data/webroot/public/css/style.css +23 -0
  40. data/webroot/public/js/application.js +31 -10
  41. data/webroot/views/jobs/list.erb +2 -0
  42. data/webroot/views/layout.erb +3 -1
  43. data/webroot/views/notifications/index.erb +16 -0
  44. data/webroot/views/resources/list.erb +12 -7
  45. data/webroot/views/sidebar.erb +2 -0
  46. metadata +11 -2
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.0.8
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{coupler}
8
- s.version = "0.0.7"
8
+ s.version = "0.0.8"
9
9
  s.platform = %q{java}
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ["Jeremy Stephens"]
13
- s.date = %q{2011-07-19}
13
+ s.date = %q{2011-08-16}
14
14
  s.default_executable = %q{coupler}
15
15
  s.description = %q{Coupler is a (JRuby) desktop application designed to link datasets together}
16
16
  s.email = %q{jeremy.f.stephens@vanderbilt.edu}
@@ -59,6 +59,10 @@ Gem::Specification.new do |s|
59
59
  "db/migrate/021_add_fields_to_connections.rb",
60
60
  "db/migrate/022_remove_database_name_from_resources.rb",
61
61
  "db/migrate/023_add_import_jobs.rb",
62
+ "db/migrate/024_add_error_msg_to_jobs.rb",
63
+ "db/migrate/025_add_notifications.rb",
64
+ "db/migrate/026_add_status_to_resources.rb",
65
+ "db/migrate/027_add_notification_id_to_jobs.rb",
62
66
  "features/connections.feature",
63
67
  "features/matchers.feature",
64
68
  "features/projects.feature",
@@ -86,6 +90,7 @@ Gem::Specification.new do |s|
86
90
  "lib/coupler/extensions/imports.rb",
87
91
  "lib/coupler/extensions/jobs.rb",
88
92
  "lib/coupler/extensions/matchers.rb",
93
+ "lib/coupler/extensions/notifications.rb",
89
94
  "lib/coupler/extensions/projects.rb",
90
95
  "lib/coupler/extensions/resources.rb",
91
96
  "lib/coupler/extensions/results.rb",
@@ -104,6 +109,7 @@ Gem::Specification.new do |s|
104
109
  "lib/coupler/models/job.rb",
105
110
  "lib/coupler/models/jobify.rb",
106
111
  "lib/coupler/models/matcher.rb",
112
+ "lib/coupler/models/notification.rb",
107
113
  "lib/coupler/models/project.rb",
108
114
  "lib/coupler/models/resource.rb",
109
115
  "lib/coupler/models/result.rb",
@@ -138,6 +144,7 @@ Gem::Specification.new do |s|
138
144
  "test/functional/test_imports.rb",
139
145
  "test/functional/test_jobs.rb",
140
146
  "test/functional/test_matchers.rb",
147
+ "test/functional/test_notifications.rb",
141
148
  "test/functional/test_projects.rb",
142
149
  "test/functional/test_resources.rb",
143
150
  "test/functional/test_results.rb",
@@ -158,6 +165,7 @@ Gem::Specification.new do |s|
158
165
  "test/unit/models/test_import.rb",
159
166
  "test/unit/models/test_job.rb",
160
167
  "test/unit/models/test_matcher.rb",
168
+ "test/unit/models/test_notification.rb",
161
169
  "test/unit/models/test_project.rb",
162
170
  "test/unit/models/test_resource.rb",
163
171
  "test/unit/models/test_result.rb",
@@ -242,6 +250,7 @@ Gem::Specification.new do |s|
242
250
  "webroot/views/layout.erb",
243
251
  "webroot/views/matchers/form.erb",
244
252
  "webroot/views/matchers/list.erb",
253
+ "webroot/views/notifications/index.erb",
245
254
  "webroot/views/projects/form.erb",
246
255
  "webroot/views/projects/index.erb",
247
256
  "webroot/views/projects/show.erb",
@@ -0,0 +1,5 @@
1
+ Sequel.migration do
2
+ up do
3
+ add_column :jobs, :error_msg, String, :text => true
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table(:notifications) do
4
+ primary_key :id
5
+ String :message
6
+ String :url
7
+ TrueClass :seen
8
+ Integer :import_id
9
+ Time :created_at
10
+ Time :updated_at
11
+ end
12
+ end
13
+ down do
14
+ drop_table(:notifications)
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ Sequel.migration do
2
+ up do
3
+ [:resources, :resources_versions].each do |name|
4
+ add_column name, :status, String
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ Sequel.migration do
2
+ up do
3
+ add_column :jobs, :notification_id, Integer
4
+ end
5
+ down do
6
+ drop_column :jobs, :notification_id
7
+ end
8
+ end
@@ -21,8 +21,12 @@ require 'json'
21
21
  require 'fastercsv'
22
22
  require 'carrierwave/sequel'
23
23
  require 'mongrel'
24
- #require 'jdbc/mysql' # Sequel should load this when it needs to.
25
- #require 'jdbc/h2' # Sequel should load this when it needs to.
24
+
25
+ =begin
26
+ # Sequel will automatically include these as needed
27
+ require 'jdbc/mysql'
28
+ require 'jdbc/h2'
29
+ =end
26
30
 
27
31
  module Coupler
28
32
  def self.environment
@@ -25,6 +25,7 @@ module Coupler
25
25
  register Extensions::Jobs
26
26
  register Extensions::Transformers
27
27
  register Extensions::Imports
28
+ register Extensions::Notifications
28
29
  register Extensions::Exceptions
29
30
 
30
31
  helpers do
@@ -3,6 +3,8 @@ module Coupler
3
3
  end
4
4
  end
5
5
 
6
+ require File.dirname(__FILE__) + "/extensions/exceptions"
7
+
6
8
  require File.dirname(__FILE__) + "/extensions/connections"
7
9
  require File.dirname(__FILE__) + "/extensions/projects"
8
10
  require File.dirname(__FILE__) + "/extensions/resources"
@@ -13,4 +15,4 @@ require File.dirname(__FILE__) + "/extensions/results"
13
15
  require File.dirname(__FILE__) + "/extensions/jobs"
14
16
  require File.dirname(__FILE__) + "/extensions/transformers"
15
17
  require File.dirname(__FILE__) + "/extensions/imports"
16
- require File.dirname(__FILE__) + "/extensions/exceptions"
18
+ require File.dirname(__FILE__) + "/extensions/notifications"
@@ -9,19 +9,16 @@ module Coupler
9
9
 
10
10
  app.post "/projects/:project_id/imports" do
11
11
  @import = Models::Import.new(params[:import].merge(:project_id => @project.id))
12
-
13
- if @import.save
14
- @resource = Models::Resource.new(:import => @import)
15
- if @resource.valid?
16
- if @import.import!
17
- @resource.save
18
- redirect("/projects/#{@project.id}/resources/#{@resource.id}")
19
- else
20
- redirect("/projects/#{@project.id}/imports/#{@import.id}/edit")
21
- end
22
- end
12
+ @resource = Models::Resource.new(:name => @import.name, :project_id => @project.id, :status => 'pending')
13
+ if @import.valid? && @resource.valid?
14
+ @import.save
15
+ @resource.import = @import
16
+ @resource.save
17
+ Scheduler.instance.schedule_import_job(@import)
18
+ redirect("/projects/#{@project.id}/resources")
19
+ else
20
+ erb(:'imports/new')
23
21
  end
24
- erb(:'imports/new')
25
22
  end
26
23
 
27
24
  app.get "/projects/:project_id/imports/:id/edit" do
@@ -34,8 +31,8 @@ module Coupler
34
31
  @import = Models::Import[:id => params[:id], :project_id => @project.id]
35
32
  raise ImportNotFound unless @import
36
33
  @import.repair_duplicate_keys!(params[:delete])
37
- @resource = Models::Resource.create(:import => @import)
38
- redirect("/projects/#{@project.id}/resources/#{@resource.id}")
34
+ @import.resource.activate!
35
+ redirect("/projects/#{@project.id}/resources/#{@import.resource.id}")
39
36
  end
40
37
  end
41
38
  end
@@ -0,0 +1,26 @@
1
+ module Coupler
2
+ module Extensions
3
+ module Notifications
4
+ include Models
5
+
6
+ def self.registered(app)
7
+ app.before do
8
+ Notification.filter(~{:seen => true}, {:url => request.path_info}).update(:seen => true)
9
+ end
10
+
11
+ app.get "/notifications" do
12
+ @notifications = Notification.order(:created_at).all
13
+ erb :"notifications/index"
14
+ end
15
+
16
+ app.get "/notifications/unseen.json" do
17
+ content_type 'application/json'
18
+ notifications = Notification.filter(~{:seen => true}).order(:created_at).all
19
+ notifications.collect do |n|
20
+ { 'id' => n.id, 'message' => n.message, 'url' => n.url, 'created_at' => n.created_at.iso8601 }
21
+ end.to_json
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -16,6 +16,7 @@ module Coupler
16
16
  autoload :Scenario, File.dirname(__FILE__) + "/models/scenario"
17
17
  autoload :Transformation, File.dirname(__FILE__) + "/models/transformation"
18
18
  autoload :Transformer, File.dirname(__FILE__) + "/models/transformer"
19
+ autoload :Notification, File.dirname(__FILE__) + "/models/notification"
19
20
  end
20
21
  end
21
22
 
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Comparison < Sequel::Model
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Connection < Sequel::Model
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Field < Sequel::Model
@@ -6,13 +5,14 @@ module Coupler
6
5
  many_to_one :resource
7
6
  one_to_many :transformations, :key => :source_field_id
8
7
 
9
- def original_column_options
10
- { :name => name, :type => db_type, :primary_key => is_primary_key }
11
- end
8
+ TYPES = {
9
+ 'integer' => {:type => Integer},
10
+ 'string' => {:type => String, :size => 255},
11
+ 'datetime' => {:type => DateTime}
12
+ }
12
13
 
13
14
  def local_column_options
14
- { :name => name, :type => final_db_type,
15
- :primary_key => is_primary_key }
15
+ { :name => name, :primary_key => is_primary_key }.merge!(TYPES[final_type])
16
16
  end
17
17
 
18
18
  def final_type
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Import < Sequel::Model
@@ -15,6 +14,7 @@ module Coupler
15
14
  \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x
16
15
 
17
16
  many_to_one :project
17
+ one_to_one :resource
18
18
  plugin :serialization
19
19
  serialize_attributes :marshal, :field_types, :field_names
20
20
  mount_uploader :data, DataUploader
@@ -50,7 +50,7 @@ module Coupler
50
50
  @preview
51
51
  end
52
52
 
53
- def import!
53
+ def import!(&progress)
54
54
  project.local_database do |db|
55
55
  column_info = []
56
56
  column_names = []
@@ -79,7 +79,9 @@ module Coupler
79
79
  buffer = ImportBuffer.new(column_names, ds)
80
80
  skip = has_headers
81
81
  primary_key_index = field_names.index(primary_key_name)
82
- FasterCSV.foreach(data.file.file) do |row|
82
+ io = File.open(data.file.file, 'rb')
83
+ csv = FasterCSV.new(io)
84
+ csv.each do |row|
83
85
  if skip
84
86
  # skip header if necessary
85
87
  skip = false
@@ -97,6 +99,10 @@ module Coupler
97
99
  self.has_duplicate_keys = true if num > 1
98
100
 
99
101
  buffer.add(row)
102
+
103
+ if block_given?
104
+ yield io.pos
105
+ end
100
106
  end
101
107
  buffer.flush
102
108
 
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Job < Sequel::Model
@@ -6,6 +5,8 @@ module Coupler
6
5
 
7
6
  many_to_one :resource
8
7
  many_to_one :scenario
8
+ many_to_one :import
9
+ many_to_one :notification
9
10
 
10
11
  def percent_completed
11
12
  total > 0 ? completed * 100 / total : 0
@@ -13,31 +14,74 @@ module Coupler
13
14
 
14
15
  def execute
15
16
  Logger.instance.info("Starting job #{id} (#{name})")
17
+
18
+ opts = {}
16
19
  case name
17
20
  when 'transform'
18
- update(:status => 'running', :started_at => Time.now, :total => resource.source_dataset_count)
19
-
20
- new_status = 'failed'
21
- begin
22
- resource.transform! { |n| update(:completed => completed + n) }
23
- new_status = 'done'
24
- ensure
25
- update(:status => new_status, :completed_at => Time.now)
26
- end
21
+ opts[:total] = resource.source_dataset_count
22
+ when 'import'
23
+ opts[:total] = import.data.file.size
24
+ end
27
25
 
28
- when 'run_scenario'
29
- update(:status => 'running', :started_at => Time.now)
26
+ begin
27
+ opts[:status] = 'running'
28
+ opts[:started_at] = Time.now
29
+ update(opts)
30
+ send("execute_#{name}")
31
+ rescue Exception => e
32
+ message = "%s: %s\n %s" % [e.class.to_s, e.to_s, e.backtrace.join("\n ")]
33
+ update({
34
+ :status => 'failed',
35
+ :completed_at => Time.now,
36
+ :error_msg => message
37
+ })
38
+ Logger.instance.error("Job #{id} (#{name}) crashed: #{message}")
39
+ raise e
40
+ end
41
+ self.status = 'done'
42
+ self.completed_at = Time.now
43
+ save
44
+ Logger.instance.info("Job #{id} (#{name}) finished successfully")
45
+ end
46
+
47
+ private
30
48
 
31
- new_status = 'failed'
32
- begin
33
- scenario.run!
34
- new_status = 'done'
35
- ensure
36
- update(:status => new_status, :completed_at => Time.now)
49
+ def execute_transform
50
+ resource.transform! { |n| update(:completed => completed + n) }
51
+ end
52
+
53
+ def execute_run_scenario
54
+ scenario.run!
55
+ end
56
+
57
+ def execute_import
58
+ last = Time.now # don't slam the database
59
+ result = import.import! do |pos|
60
+ now = Time.now
61
+ if now - last >= 1
62
+ last = now
63
+ update(:completed => pos)
64
+ end
65
+ end
66
+ if result
67
+ # NOTE: This is a bug waiting to happen. Import doesn't verify
68
+ # that it has a resource, but supposedly, it will always have
69
+ # one. The resource gets created at the same time in the
70
+ # controller.
71
+
72
+ resource = import.resource
73
+ resource.activate!
74
+ self.notification = Notification.create({
75
+ :message => "Import finished successfully",
76
+ :url => "/projects/#{import.project_id}/resources/#{resource.id}"
77
+ })
78
+ else
79
+ self.notification = Notification.create({
80
+ :message => "Import finished, but with errors",
81
+ :url => "/projects/#{import.project_id}/imports/#{import.id}/edit"
82
+ })
37
83
  end
38
84
  end
39
- Logger.instance.info("Job #{id} (#{name}) finished")
40
- end
41
85
  end
42
86
  end
43
87
  end
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Matcher < Sequel::Model
@@ -0,0 +1,7 @@
1
+ module Coupler
2
+ module Models
3
+ class Notification < Sequel::Model
4
+ include CommonModel
5
+ end
6
+ end
7
+ end
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Project < Sequel::Model
@@ -1,4 +1,3 @@
1
- pp caller
2
1
  module Coupler
3
2
  module Models
4
3
  class Resource < Sequel::Model
@@ -30,8 +29,6 @@ module Coupler
30
29
  def import=(*args)
31
30
  result = super
32
31
  if new?
33
- self.project = import.project
34
- self.name = import.name
35
32
  self.table_name = "import_#{import.id}"
36
33
  end
37
34
  result
@@ -106,21 +103,20 @@ module Coupler
106
103
  end
107
104
  end
108
105
 
109
- def status
110
- if transformed_with.to_s != transformation_ids.join(",") || transformations_dataset.filter("updated_at > ?", transformed_at).count > 0
111
- "out_of_date"
112
- else
113
- "ok"
114
- end
115
- end
116
-
117
106
  def scenarios
118
107
  Scenario.filter(["resource_1_id = ? OR resource_2_id = ?", id, id]).all
119
108
  end
120
109
 
121
- def refresh_fields!
110
+ def transformations_updated!
111
+ last_updated_at = transformed_at
112
+ transformation_ids = []
113
+
122
114
  fields_dataset.update(:local_db_type => nil, :local_type => nil)
123
115
  transformations_dataset.order(:position).each do |transformation|
116
+ transformation_ids << transformation.id
117
+ if last_updated_at.nil? || transformation.updated_at > last_updated_at
118
+ last_updated_at = transformation.updated_at
119
+ end
124
120
  if transformation.source_field_id == transformation.result_field_id
125
121
  source_field = transformation.source_field
126
122
  changes = transformation.field_changes[source_field.id]
@@ -130,6 +126,12 @@ module Coupler
130
126
  })
131
127
  end
132
128
  end
129
+
130
+ if transformed_with.to_s != transformation_ids.join(",") || (last_updated_at && last_updated_at > transformed_at)
131
+ update(:status => "out_of_date")
132
+ else
133
+ update(:status => "ok")
134
+ end
133
135
  end
134
136
 
135
137
  def transform!(&progress)
@@ -137,6 +139,7 @@ module Coupler
137
139
  create_local_table!
138
140
  _transform(&progress)
139
141
  self.update({
142
+ :status => 'ok',
140
143
  :transformed_at => Time.now,
141
144
  :transformed_with => t_ids
142
145
  })
@@ -161,6 +164,13 @@ module Coupler
161
164
  primary_key_name.to_sym
162
165
  end
163
166
 
167
+ # Activate resource that was pending until import was completed
168
+ def activate!
169
+ set_primary_key
170
+ create_fields
171
+ update(:status => "ok")
172
+ end
173
+
164
174
  private
165
175
  def transformation_ids
166
176
  transformations_dataset.select(:id).order(:id).all.collect(&:id)
@@ -170,6 +180,15 @@ module Coupler
170
180
  Coupler.connection_string("project_#{project.id}")
171
181
  end
172
182
 
183
+ def set_primary_key
184
+ source_database do |db|
185
+ schema = db.schema(table_name.to_sym)
186
+ info = schema.detect { |x| x[1][:primary_key] }
187
+ self.primary_key_name = info[0].to_s
188
+ self.primary_key_type = info[1][:type].to_s
189
+ end
190
+ end
191
+
173
192
  def create_fields
174
193
  source_schema.each do |(name, info)|
175
194
  add_field({
@@ -233,19 +252,21 @@ module Coupler
233
252
  validates_presence [:project_id, :name]
234
253
  validates_presence :slug
235
254
  validates_unique [:name, :project_id], [:slug, :project_id]
236
- validates_presence [:table_name]
237
-
238
- if import.nil? && errors.on(:table_name).nil?
239
- source_database do |db|
240
- sym = self.table_name.to_sym
241
- if !db.tables.include?(sym)
242
- errors.add(:table_name, "is invalid")
243
- else
244
- keys = db.schema(sym).select { |info| info[1][:primary_key] }
245
- if keys.empty?
246
- errors.add(:table_name, "doesn't have a primary key")
247
- elsif keys.length > 1
248
- errors.add(:table_name, "has too many primary keys")
255
+
256
+ if status != 'pending'
257
+ validates_presence [:table_name]
258
+ if errors.on(:table_name).nil?
259
+ source_database do |db|
260
+ sym = self.table_name.to_sym
261
+ if !db.tables.include?(sym)
262
+ errors.add(:table_name, "is invalid")
263
+ else
264
+ keys = db.schema(sym).select { |info| info[1][:primary_key] }
265
+ if keys.empty?
266
+ errors.add(:table_name, "doesn't have a primary key")
267
+ elsif keys.length > 1
268
+ errors.add(:table_name, "has too many primary keys")
269
+ end
249
270
  end
250
271
  end
251
272
  end
@@ -257,11 +278,9 @@ module Coupler
257
278
  # NOTE: I'm doing this instead of using before_create because
258
279
  # serialization happens in before_save, which gets called before
259
280
  # the before_create hook
260
- source_database do |db|
261
- schema = db.schema(table_name.to_sym)
262
- info = schema.detect { |x| x[1][:primary_key] }
263
- self.primary_key_name = info[0].to_s
264
- self.primary_key_type = info[1][:type].to_s
281
+ if status != "pending"
282
+ set_primary_key
283
+ self.status = "ok"
265
284
  end
266
285
  end
267
286
  super
@@ -269,7 +288,9 @@ module Coupler
269
288
 
270
289
  def after_create
271
290
  super
272
- create_fields
291
+ if status != "pending"
292
+ create_fields
293
+ end
273
294
  end
274
295
 
275
296
  def after_destroy