coupler 0.0.7-java → 0.0.8-java

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 (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