oculus 0.5.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -5,3 +5,4 @@ rvm:
5
5
  before_script:
6
6
  - "export DISPLAY=:99.0"
7
7
  - "sh -e /etc/init.d/xvfb start"
8
+ - "bundle exec rake db:test:populate"
data/Rakefile CHANGED
@@ -3,6 +3,7 @@ require "bundler/gem_tasks"
3
3
 
4
4
  require 'rspec/core/rake_task'
5
5
  require 'cucumber/rake/task'
6
+ require 'mysql2'
6
7
 
7
8
  desc 'Run RSpec tests'
8
9
  RSpec::Core::RakeTask.new(:spec) do |task|
@@ -15,4 +16,28 @@ Cucumber::Rake::Task.new(:cucumber) do |task|
15
16
  task.cucumber_opts = 'features --format pretty'
16
17
  end
17
18
 
19
+ namespace :db do
20
+ namespace :test do
21
+ desc "Populate the test database"
22
+ task :populate do
23
+ client = Mysql2::Client.new(:host => "localhost", :username => "root")
24
+ client.query "CREATE DATABASE IF NOT EXISTS oculus_test"
25
+ client.query "USE oculus_test"
26
+ client.query %[
27
+ CREATE TABLE IF NOT EXISTS oculus_users (
28
+ id MEDIUMINT NOT NULL AUTO_INCREMENT,
29
+ name VARCHAR(255),
30
+ PRIMARY KEY (id)
31
+ );
32
+ ]
33
+
34
+ client.query 'TRUNCATE oculus_users'
35
+
36
+ client.query %[
37
+ INSERT INTO oculus_users (name) VALUES ('Paul'), ('Amy'), ('Peter')
38
+ ]
39
+ end
40
+ end
41
+ end
42
+
18
43
  task :default => [:spec, :cucumber]
data/TODO.md CHANGED
@@ -3,7 +3,6 @@
3
3
  ## Upcoming (pre-1.0)
4
4
 
5
5
  * Input validation
6
- * Data download
7
6
 
8
7
  ## Eventually (1.1 or later)
9
8
 
@@ -1,5 +1,9 @@
1
1
  Given /^a query is cached with results:$/ do |results|
2
- Oculus::Query.create(:name => "all users", :query => "SELECT * FROM oculus_users", :results => results.raw)
2
+ Oculus::Query.create(:name => "all users",
3
+ :query => "SELECT * FROM oculus_users",
4
+ :results => results.raw,
5
+ :started_at => Time.now,
6
+ :finished_at => Time.now)
3
7
  end
4
8
 
5
9
  Given /^I am on the history page$/ do
@@ -5,12 +5,13 @@ require 'oculus/server'
5
5
  require 'capybara/cucumber'
6
6
 
7
7
  Capybara.app = Oculus::Server
8
+ Capybara.default_wait_time = 10
8
9
 
9
10
  Oculus.cache_path = 'tmp/test_cache'
10
11
  Oculus.connection_options = {
11
12
  :host => 'localhost',
12
13
  :username => 'root',
13
- :database => 'test'
14
+ :database => 'oculus_test'
14
15
  }
15
16
 
16
17
  Before do
@@ -3,8 +3,34 @@ require 'delegate'
3
3
  module Oculus
4
4
  module Presenters
5
5
  class QueryPresenter < SimpleDelegator
6
- def formatted_date
7
- date.strftime("%Y-%m-%d %H:%M") if date
6
+ def formatted_start_time
7
+ started_at.strftime("%Y-%m-%d %I:%M %p") if started_at
8
+ end
9
+
10
+ def formatted_finish_time
11
+ finished_at.strftime("%Y-%m-%d %I:%M %p") if finished_at
12
+ end
13
+
14
+ def elapsed_time
15
+ return "" unless started_at && finished_at
16
+
17
+ seconds = (finished_at - started_at).round
18
+
19
+ if seconds < 60
20
+ "#{seconds} seconds"
21
+ else
22
+ minutes = (seconds / 60).floor
23
+ seconds %= 60
24
+
25
+ if minutes < 60
26
+ "#{minutes} minutes #{seconds} seconds"
27
+ else
28
+ hours = (minutes / 60).floor
29
+ minutes %= 60
30
+
31
+ "#{hours} hours #{minutes} minutes"
32
+ end
33
+ end
8
34
  end
9
35
 
10
36
  def status
data/lib/oculus/query.rb CHANGED
@@ -6,7 +6,9 @@ module Oculus
6
6
  attr_accessor :query
7
7
  attr_accessor :results
8
8
  attr_accessor :error
9
- attr_accessor :date
9
+ attr_accessor :started_at
10
+ attr_accessor :finished_at
11
+ attr_accessor :starred
10
12
  attr_accessor :thread_id
11
13
 
12
14
  def initialize(attributes = {})
@@ -20,7 +22,9 @@ module Oculus
20
22
  :name => name,
21
23
  :author => author,
22
24
  :query => query,
23
- :date => date,
25
+ :started_at => started_at,
26
+ :finished_at => finished_at,
27
+ :starred => starred,
24
28
  :thread_id => thread_id
25
29
  }
26
30
  attrs[:error] = error if error
@@ -28,24 +32,40 @@ module Oculus
28
32
  end
29
33
 
30
34
  def execute(connection)
31
- self.results = connection.execute(query)
35
+ self.started_at = Time.now
36
+ self.thread_id = connection.thread_id
37
+ self.save
38
+ results = connection.execute(query)
32
39
  rescue Connection::Error => e
33
- self.error = e.message
40
+ error = e.message
41
+ ensure
42
+ reload
43
+ self.results = results if results
44
+ self.error = error if error
45
+ self.finished_at = Time.now
46
+ self.save
34
47
  end
35
48
 
36
49
  def save
37
- @date = Time.now
38
50
  Oculus.data_store.save_query(self)
39
51
  end
40
52
 
41
53
  def complete?
42
- !!error || (!results.nil? && !results.empty?)
54
+ !!finished_at
43
55
  end
44
56
 
45
57
  def succeeded?
46
58
  complete? && !error
47
59
  end
48
60
 
61
+ def to_csv
62
+ CSV.generate do |csv|
63
+ results.each do |row|
64
+ csv << row
65
+ end
66
+ end
67
+ end
68
+
49
69
  class << self
50
70
  def create(attributes)
51
71
  query = new(attributes)
@@ -54,8 +74,17 @@ module Oculus
54
74
  end
55
75
 
56
76
  def find(id)
57
- Oculus.data_store.load_query(id)
77
+ new(Oculus.data_store.load_query(id))
58
78
  end
59
79
  end
80
+
81
+ private
82
+
83
+ def reload
84
+ Oculus.data_store.load_query(id).each do |attr, value|
85
+ send("#{attr}=", value)
86
+ end
87
+ end
88
+
60
89
  end
61
90
  end
@@ -102,6 +102,18 @@ body {
102
102
  margin-top: 1em;
103
103
  }
104
104
 
105
+ button.star .starred-contents {
106
+ display: none;
107
+ }
108
+
109
+ button.starred .starred-contents {
110
+ display: inline;
111
+ }
112
+
113
+ button.starred .unstarred-contents {
114
+ display: none;
115
+ }
116
+
105
117
  #query-properties-form {
106
118
  display: none;
107
119
  }
@@ -32,6 +32,22 @@ $(function() {
32
32
  });
33
33
  });
34
34
 
35
+ $('#query-actions .star').click(function() {
36
+ var btn = $(this),
37
+ newState = !btn.hasClass('starred');
38
+
39
+ $.ajax({
40
+ url: this.href,
41
+ type: 'PUT',
42
+ data: {
43
+ 'starred': newState
44
+ },
45
+ success: function() {
46
+ btn.toggleClass('starred', newState);
47
+ }
48
+ });
49
+ });
50
+
35
51
  $('#query-actions .edit').click(function() {
36
52
  $('#query-properties-form').toggle();
37
53
  });
@@ -13,7 +13,7 @@
13
13
  <% @queries.each do |query| %>
14
14
  <tr>
15
15
  <td class="status"><span class="indicator indicator-<%= query.status %>">*</span></td>
16
- <td class="date"><%= query.formatted_date %></td>
16
+ <td class="date"><%= query.formatted_start_time %></td>
17
17
  <td>
18
18
  <a href="/queries/<%= query.id %>"><%= query.description %></a>
19
19
  </td>
@@ -27,9 +27,3 @@
27
27
  <% end %>
28
28
  </tbody>
29
29
  </table>
30
-
31
- <script type="text/javascript">
32
- CodeMirror.fromTextArea(document.getElementById('query-field'), {
33
- tabSize: 2
34
- });
35
- </script>
@@ -20,6 +20,9 @@
20
20
  <li<% if @current_tab == 'home' %> class="active"<% end %>>
21
21
  <a href="/">Home</a>
22
22
  </li>
23
+ <li<% if @current_tab == 'starred' %> class="active"<% end %>>
24
+ <a href="/starred">Starred</a>
25
+ </li>
23
26
  <li<% if @current_tab == 'history' %> class="active"<% end %>>
24
27
  <a href="/history">History</a>
25
28
  </li>
@@ -1,17 +1,35 @@
1
1
  <h1 class="description"><%= @query.description %></h1>
2
2
 
3
3
  <div id="query-actions">
4
+ <button class="btn btn-success star<% if @query.starred %> starred<% end %>">
5
+ <span class="unstarred-contents">
6
+ <i class="icon-star-empty icon-white"></i>
7
+ Star
8
+ </span>
9
+ <span class="starred-contents">
10
+ <i class="icon-star icon-white"></i>
11
+ Starred
12
+ </span>
13
+ </button>
4
14
  <button class="btn edit<% unless @query.named? %> active<% end %>" data-toggle="button">
5
15
  <i class="icon-pencil"></i>
6
16
  Edit
7
17
  </button>
18
+ <a class="btn file<% unless @query.succeeded? && @headers %> disabled<% end %>" href="/queries/<%= @query.id %>/download">
19
+ <i class="icon-file"></i>
20
+ Download
21
+ </a>
8
22
  </div>
9
23
 
10
24
  <dl class="dl-horizontal query-properties">
11
25
  <dt>Author</dt>
12
26
  <dd class="author"><%= @query.author %>&nbsp;</dd>
13
- <dt>Timestamp</dt>
14
- <dd><%= @query.formatted_date %></dd>
27
+ <dt>Started at</dt>
28
+ <dd><%= @query.formatted_start_time %></dd>
29
+ <% if @query.finished_at %>
30
+ <dt>Finished at</dt>
31
+ <dd><%= @query.formatted_finish_time %> (<%= @query.elapsed_time %>)</dd>
32
+ <% end %>
15
33
  </dl>
16
34
 
17
35
  <form class="well form-inline" id="query-properties-form">
@@ -23,24 +41,31 @@
23
41
  <pre class="cm-s-default"><%= @query.query %></pre>
24
42
 
25
43
  <% if @query.succeeded? %>
26
- <table class="table table-condensed" id="results-table">
27
- <thead>
28
- <tr>
29
- <% @headers.each do |label| %>
30
- <th><%= label %></th>
31
- <% end %>
32
- </tr>
33
- </thead>
34
- <tbody class="results">
35
- <% @results.each do |result| %>
44
+ <% if @headers %>
45
+ <table class="table table-condensed" id="results-table">
46
+ <thead>
36
47
  <tr>
37
- <% result.each do |value| %>
38
- <td><%= value %></td>
48
+ <% @headers.each do |label| %>
49
+ <th><%= label %></th>
39
50
  <% end %>
40
51
  </tr>
41
- <% end %>
42
- </tbody>
43
- </table>
52
+ </thead>
53
+ <tbody class="results">
54
+ <% @results.each do |result| %>
55
+ <tr>
56
+ <% result.each do |value| %>
57
+ <td><%= value %></td>
58
+ <% end %>
59
+ </tr>
60
+ <% end %>
61
+ </tbody>
62
+ </table>
63
+ <% else %>
64
+ <div class="alert alert-success">
65
+ <strong>Heads Up!</strong>
66
+ This query ran successfully, but returned no results.
67
+ </div>
68
+ <% end %>
44
69
  <% elsif @query.error %>
45
70
  <div class="alert alert-error">
46
71
  <strong>Sorry!</strong>
@@ -48,7 +73,7 @@
48
73
  </div>
49
74
  <% else %>
50
75
  <div class="alert alert-info">
51
- <strong>Head's Up!</strong>
76
+ <strong>Heads Up!</strong>
52
77
  This query is in progress, and has not returned any results yet.
53
78
  </div>
54
79
  <% end %>
@@ -56,4 +81,7 @@
56
81
  <script type="text/javascript">
57
82
  var queryNode = $('pre.cm-s-default');
58
83
  CodeMirror.runMode(queryNode.text(), 'mysql', queryNode[0]);
84
+ $('a.disabled').click(function(e) {
85
+ e.preventDefault();
86
+ });
59
87
  </script>
@@ -0,0 +1,29 @@
1
+ <% @current_tab = 'starred' %>
2
+
3
+ <h2>Starred Queries</h2>
4
+ <table class="table">
5
+ <thead>
6
+ <th></th>
7
+ <th>Timestamp</th>
8
+ <th>Query</th>
9
+ <th>Author</th>
10
+ <th></th>
11
+ </thead>
12
+ <tbody id="history">
13
+ <% @queries.each do |query| %>
14
+ <tr>
15
+ <td class="status"><span class="indicator indicator-<%= query.status %>">*</span></td>
16
+ <td class="date"><%= query.formatted_start_time %></td>
17
+ <td>
18
+ <a href="/queries/<%= query.id %>"><%= query.description %></a>
19
+ </td>
20
+ <td class="author">
21
+ <%= query.author %>
22
+ </td>
23
+ <td>
24
+ <a class="delete" href="/queries/<%= query.id %>"><i class="icon-remove"></i></a>
25
+ </td>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
data/lib/oculus/server.rb CHANGED
@@ -21,6 +21,12 @@ module Oculus
21
21
  erb :index
22
22
  end
23
23
 
24
+ get '/starred' do
25
+ @queries = Oculus.data_store.starred_queries.map { |q| Oculus::Presenters::QueryPresenter.new(q) }
26
+
27
+ erb :starred
28
+ end
29
+
24
30
  get '/history' do
25
31
  @queries = Oculus.data_store.all_queries.map { |q| Oculus::Presenters::QueryPresenter.new(q) }
26
32
 
@@ -35,13 +41,12 @@ module Oculus
35
41
  end
36
42
 
37
43
  post '/queries' do
38
- connection = Oculus::Connection::Mysql2.new(Oculus.connection_options)
39
- query = Oculus::Query.create(:query => params[:query],
40
- :thread_id => connection.thread_id)
44
+ query = Oculus::Query.create(:query => params[:query])
41
45
 
42
46
  pid = fork do
47
+ query = Oculus::Query.find(query.id)
48
+ connection = Oculus::Connection::Mysql2.new(Oculus.connection_options)
43
49
  query.execute(connection)
44
- query.save
45
50
  end
46
51
 
47
52
  Process.detach(pid)
@@ -67,14 +72,26 @@ module Oculus
67
72
  erb :show
68
73
  end
69
74
 
75
+ get '/queries/:id/download' do
76
+ query = Oculus::Query.find(params[:id])
77
+ timestamp = query.started_at.strftime('%Y%m%d%H%M')
78
+
79
+ attachment "#{timestamp}-query-#{query.id}-results.csv"
80
+ content_type "text/csv"
81
+ last_modified query.finished_at
82
+
83
+ query.to_csv
84
+ end
85
+
70
86
  get '/queries/:id/status' do
71
87
  Oculus::Presenters::QueryPresenter.new(Oculus::Query.find(params[:id])).status
72
88
  end
73
89
 
74
90
  put '/queries/:id' do
75
91
  @query = Oculus::Query.find(params[:id])
76
- @query.name = params[:name]
77
- @query.author = params[:author]
92
+ @query.name = params[:name] if params[:name]
93
+ @query.author = params[:author] if params[:author]
94
+ @query.starred = params[:starred] == "true" if params[:starred]
78
95
  @query.save
79
96
 
80
97
  puts "true"
@@ -10,7 +10,13 @@ module Oculus
10
10
 
11
11
  def all_queries
12
12
  Dir["#{root}/*.query"].map do |path|
13
- File.parse(path)
13
+ Query.new(File.parse(path))
14
+ end.sort { |a,b| b.id <=> a.id }
15
+ end
16
+
17
+ def starred_queries
18
+ Dir["#{root}/starred/*.query"].map do |path|
19
+ Query.new(File.parse(path))
14
20
  end.sort { |a,b| b.id <=> a.id }
15
21
  end
16
22
 
@@ -19,7 +25,16 @@ module Oculus
19
25
 
20
26
  File.open(filename_for_id(query.id), 'w') do |file|
21
27
  file.write_prelude(query.attributes)
22
- file.write_results(query.results) if query.results
28
+ file.write_results(query.results) if query.results && query.results.length > 0
29
+ end
30
+
31
+ FileUtils.mkdir_p(File.join(root, "starred")) unless Dir.exist?(File.join(root, "starred"))
32
+ star_path = starred_filename_for_id(query.id)
33
+
34
+ if query.starred
35
+ File.symlink(File.expand_path(filename_for_id(query.id)), star_path) unless File.exist?(star_path)
36
+ elsif File.exist?(star_path)
37
+ File.unlink(star_path)
23
38
  end
24
39
  end
25
40
 
@@ -34,6 +49,9 @@ module Oculus
34
49
  end
35
50
 
36
51
  def delete_query(id)
52
+ star_path = starred_filename_for_id(id)
53
+ File.unlink(star_path) if File.exist?(star_path)
54
+
37
55
  path = filename_for_id(id)
38
56
 
39
57
  if File.exist?(path)
@@ -50,9 +68,9 @@ module Oculus
50
68
  file = File.open(path)
51
69
  attributes = file.attributes
52
70
  attributes[:results] = file.results
53
- Oculus::Query.new(attributes).tap do |query|
54
- query.id = File.basename(path).split('.').first.to_i
55
- end
71
+ attributes[:id] = File.basename(path).split('.').first.to_i
72
+ attributes[:starred] ||= false
73
+ attributes
56
74
  end
57
75
 
58
76
  def write_prelude(attributes)
@@ -93,6 +111,11 @@ module Oculus
93
111
  end
94
112
  end
95
113
 
114
+ def starred_filename_for_id(id)
115
+ raise ArgumentError unless id.is_a?(Integer) || id =~ /^[0-9]+/
116
+ File.join(root, "starred", "#{id}.query")
117
+ end
118
+
96
119
  def filename_for_id(id)
97
120
  raise ArgumentError unless id.is_a?(Integer) || id =~ /^[0-9]+/
98
121
  File.join(root, "#{id}.query")
@@ -1,3 +1,3 @@
1
1
  module Oculus
2
- VERSION = "0.5.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -1,33 +1,14 @@
1
1
  require 'oculus'
2
2
 
3
3
  describe Oculus::Connection do
4
- before(:all) do
5
- client = Mysql2::Client.new(:host => "localhost", :username => "root")
6
- client.query "CREATE DATABASE IF NOT EXISTS test"
7
- client.query "USE test"
8
- client.query %[
9
- CREATE TABLE IF NOT EXISTS oculus_users (
10
- id MEDIUMINT NOT NULL AUTO_INCREMENT,
11
- name VARCHAR(255),
12
- PRIMARY KEY (id)
13
- );
14
- ]
15
-
16
- client.query 'TRUNCATE oculus_users'
17
-
18
- client.query %[
19
- INSERT INTO oculus_users (name) VALUES ('Paul'), ('Amy'), ('Peter')
20
- ]
21
- end
22
-
23
- subject { Oculus::Connection::Mysql2.new(:database => 'test') }
4
+ subject { Oculus::Connection::Mysql2.new(:host => 'localhost', :database => 'oculus_test', :username => 'root') }
24
5
 
25
6
  it "fetches a result set" do
26
7
  subject.execute("SELECT * FROM oculus_users").should == [['id', 'name'], [1, 'Paul'], [2, 'Amy'], [3, 'Peter']]
27
8
  end
28
9
 
29
10
  it "returns nil for queries that don't return result sets" do
30
- query_connection = Mysql2::Client.new(:host => "localhost", :database => "test")
11
+ query_connection = Mysql2::Client.new(:host => "localhost", :database => "oculus_test", :username => "root")
31
12
  thread_id = query_connection.thread_id
32
13
  Thread.new {
33
14
  query_connection.execute("SELECT * FROM oculus_users WHERE SLEEP(2)")
@@ -38,33 +38,48 @@ describe Oculus::Storage::FileStore do
38
38
  it "round-trips a query with no results to disk" do
39
39
  query = Oculus::Query.new(:name => "Unfinished query", :author => "Me")
40
40
  subject.save_query(query)
41
- subject.load_query(query.id).results.should == []
42
- subject.load_query(query.id).query.should == query.query
43
- subject.load_query(query.id).date.should == query.date
44
- subject.load_query(query.id).author.should == query.author
45
- subject.load_query(query.id).id.should == query.id
46
- subject.load_query(query.id).thread_id.should == query.thread_id
41
+ subject.load_query(query.id).should == {
42
+ :id => query.id,
43
+ :name => query.name,
44
+ :author => query.author,
45
+ :query => query.query,
46
+ :results => [],
47
+ :thread_id => query.thread_id,
48
+ :starred => false,
49
+ :started_at => query.started_at,
50
+ :finished_at => query.finished_at
51
+ }
47
52
  end
48
53
 
49
54
  it "round-trips a query with an error to disk" do
50
55
  subject.save_query(broken_query)
51
- subject.load_query(broken_query.id).results.should == []
52
- subject.load_query(broken_query.id).error.should == broken_query.error
53
- subject.load_query(broken_query.id).query.should == broken_query.query
54
- subject.load_query(broken_query.id).date.should == broken_query.date
55
- subject.load_query(broken_query.id).author.should == broken_query.author
56
- subject.load_query(broken_query.id).id.should == broken_query.id
57
- subject.load_query(broken_query.id).thread_id.should == broken_query.thread_id
56
+ subject.load_query(broken_query.id).should == {
57
+ :id => broken_query.id,
58
+ :name => broken_query.name,
59
+ :error => broken_query.error,
60
+ :author => broken_query.author,
61
+ :query => broken_query.query,
62
+ :results => [],
63
+ :thread_id => broken_query.thread_id,
64
+ :starred => false,
65
+ :started_at => broken_query.started_at,
66
+ :finished_at => broken_query.finished_at
67
+ }
58
68
  end
59
69
 
60
70
  it "round-trips a query to disk" do
61
71
  subject.save_query(query)
62
- subject.load_query(query.id).results.should == query.results
63
- subject.load_query(query.id).query.should == query.query
64
- subject.load_query(query.id).date.should == query.date
65
- subject.load_query(query.id).author.should == query.author
66
- subject.load_query(query.id).id.should == query.id
67
- subject.load_query(query.id).thread_id.should == query.thread_id
72
+ subject.load_query(query.id).should == {
73
+ :id => query.id,
74
+ :name => query.name,
75
+ :author => query.author,
76
+ :query => query.query,
77
+ :results => query.results,
78
+ :thread_id => query.thread_id,
79
+ :starred => false,
80
+ :started_at => query.started_at,
81
+ :finished_at => query.finished_at
82
+ }
68
83
  end
69
84
 
70
85
  it "doesn't overwrite an existing query id when saving" do
@@ -87,9 +102,19 @@ describe Oculus::Storage::FileStore do
87
102
  subject.all_queries.map(&:results).should == [other_query.results, query.results]
88
103
  end
89
104
 
105
+ it "fetches starred queries" do
106
+ query.starred = true
107
+ subject.save_query(query)
108
+ subject.save_query(other_query)
109
+
110
+ results = subject.starred_queries
111
+ results.map(&:results).should == [query.results]
112
+ results.first.starred.should be true
113
+ end
114
+
90
115
  it "deletes queries" do
91
116
  subject.save_query(query)
92
- subject.load_query(query.id).name.should == query.name
117
+ subject.load_query(query.id)[:name].should == query.name
93
118
  subject.delete_query(query.id)
94
119
 
95
120
  lambda {
@@ -10,9 +10,20 @@ describe Oculus::Presenters::QueryPresenter do
10
10
  presenter.description.should == 'foo'
11
11
  end
12
12
 
13
- it "has a formatted date" do
14
- query.date = Time.mktime(2010, 1, 1, 12, 34)
15
- presenter.formatted_date.should == '2010-01-01 12:34'
13
+ it "has a formatted start time" do
14
+ query.started_at = Time.mktime(2010, 1, 1, 12, 34)
15
+ presenter.formatted_start_time.should == '2010-01-01 12:34 PM'
16
+ end
17
+
18
+ it "has a formatted finish time" do
19
+ query.finished_at = Time.mktime(2010, 1, 1, 12, 34)
20
+ presenter.formatted_finish_time.should == '2010-01-01 12:34 PM'
21
+ end
22
+
23
+ it "has an elapsed time" do
24
+ query.started_at = Time.mktime(2010, 1, 1, 10, 30)
25
+ query.finished_at = Time.mktime(2010, 1, 1, 12, 34)
26
+ presenter.elapsed_time.should == '2 hours 4 minutes'
16
27
  end
17
28
 
18
29
  it "reports successful queries" do
data/spec/query_spec.rb CHANGED
@@ -2,25 +2,25 @@ require 'oculus'
2
2
 
3
3
  describe Oculus::Query do
4
4
  before do
5
- Oculus.data_store = stub
5
+ Oculus.data_store = stub(:load_query => {}, :save_query => nil)
6
6
  end
7
7
 
8
8
  it "runs the query against the supplied connection" do
9
- connection = stub
9
+ connection = stub(:thread_id => 42)
10
10
  query = Oculus::Query.new(:query => 'SELECT * FROM users')
11
11
  connection.should_receive(:execute).with('SELECT * FROM users')
12
12
  query.execute(connection)
13
13
  end
14
14
 
15
15
  it "stores the results of running the query" do
16
- connection = stub(:execute => [['id', 'name'], [1, 'Paul']])
16
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
17
17
  query = Oculus::Query.new(:query => 'SELECT * FROM users')
18
18
  query.execute(connection)
19
19
  query.results.should == [['id', 'name'], [1, 'Paul']]
20
20
  end
21
21
 
22
22
  it "stores errors when queries fail" do
23
- connection = stub
23
+ connection = stub(:thread_id => 42)
24
24
  query = Oculus::Query.new(:query => 'SELECT * FROM users')
25
25
  connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
26
26
  query.execute(connection)
@@ -37,16 +37,39 @@ describe Oculus::Query do
37
37
  query.thread_id.should == 42
38
38
  end
39
39
 
40
- it "has a date" do
40
+ it "has a start time" do
41
41
  query = Oculus::Query.new
42
- query.date.should be nil
42
+ query.started_at.should be nil
43
43
  end
44
44
 
45
- it "updates date on save" do
46
- Oculus.data_store.stub(:save_query)
45
+ it "has a finish time" do
46
+ query = Oculus::Query.new
47
+ query.finished_at.should be nil
48
+ end
49
+
50
+ it "updates start time when executing" do
47
51
  Time.stub(:now).and_return(now = stub)
48
- query = Oculus::Query.create(:results => [['id', 'name'], [1, 'Paul']])
49
- query.date.should == now
52
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
53
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
54
+ query.execute(connection)
55
+ query.started_at.should == now
56
+ end
57
+
58
+ it "updates finish time when executing" do
59
+ Time.stub(:now).and_return(now = stub)
60
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
61
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
62
+ query.execute(connection)
63
+ query.finished_at.should == now
64
+ end
65
+
66
+ it "updates finish time when execution fails" do
67
+ Time.stub(:now).and_return(now = stub)
68
+ connection = stub(:thread_id => 42)
69
+ connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
70
+ query = Oculus::Query.new(:query => 'SELECT * FROM users')
71
+ query.execute(connection)
72
+ query.finished_at.should == now
50
73
  end
51
74
 
52
75
  it "has a name" do
@@ -59,6 +82,11 @@ describe Oculus::Query do
59
82
  query.author.should == 'Paul'
60
83
  end
61
84
 
85
+ it "can be starred" do
86
+ query = Oculus::Query.new(:starred => true)
87
+ query.starred.should == true
88
+ end
89
+
62
90
  it "stores new queries in the data store on creation" do
63
91
  Oculus.data_store.should_receive(:save_query)
64
92
  query = Oculus::Query.create(:results => [['id', 'name'], [1, 'Paul']])
@@ -75,18 +103,23 @@ describe Oculus::Query do
75
103
  Oculus::Query.find(1)
76
104
  end
77
105
 
78
- it "is not complete when no results are present" do
106
+ it "is not complete when it has not been executed" do
79
107
  query = Oculus::Query.new(:query => 'SELECT * FROM users')
80
108
  query.should_not be_complete
81
109
  end
82
110
 
83
- it "is complete when results are present" do
84
- query = Oculus::Query.new(:results => [['id', 'name'], [1, 'Paul']])
111
+ it "is complete when it has been executed" do
112
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
113
+ query = Oculus::Query.new
114
+ query.execute(connection)
85
115
  query.should be_complete
86
116
  end
87
117
 
88
118
  it "is complete when there is an error" do
89
- query = Oculus::Query.new(:error => "That's not how to write SQL")
119
+ connection = stub(:thread_id => 42)
120
+ connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
121
+ query = Oculus::Query.new
122
+ query.execute(connection)
90
123
  query.should be_complete
91
124
  end
92
125
 
@@ -96,12 +129,23 @@ describe Oculus::Query do
96
129
  end
97
130
 
98
131
  it "is successful when results are present" do
99
- query = Oculus::Query.new(:results => [['id', 'name'], [1, 'Paul']])
132
+ connection = stub(:execute => [['id', 'name'], [1, 'Paul']], :thread_id => 42)
133
+ query = Oculus::Query.new
134
+ query.execute(connection)
100
135
  query.succeeded?.should be true
101
136
  end
102
137
 
103
138
  it "is not successful when there is an error" do
104
- query = Oculus::Query.new(:error => "That's not how to write SQL")
139
+ connection = stub(:thread_id => 42)
140
+ connection.stub(:execute).and_raise(Oculus::Connection::Error.new('You have an error in your SQL syntax'))
141
+ query = Oculus::Query.new
142
+ query.execute(connection)
105
143
  query.succeeded?.should be false
106
144
  end
145
+
146
+ it "exports to CSV" do
147
+ query = Oculus::Query.new
148
+ query.results = [['id', 'name'], [1, 'Paul']]
149
+ query.to_csv.should == "id,name\n1,Paul\n"
150
+ end
107
151
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oculus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.8.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-01 00:00:00.000000000 Z
12
+ date: 2012-05-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
16
- requirement: &70342379865700 !ruby/object:Gem::Requirement
16
+ requirement: &70114293249540 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.3.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70342379865700
24
+ version_requirements: *70114293249540
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: mysql2
27
- requirement: &70342379588240 !ruby/object:Gem::Requirement
27
+ requirement: &70114293249020 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.3.11
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70342379588240
35
+ version_requirements: *70114293249020
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: vegas
38
- requirement: &70342379586780 !ruby/object:Gem::Requirement
38
+ requirement: &70114293248540 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 0.1.4
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70342379586780
46
+ version_requirements: *70114293248540
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rake
49
- requirement: &70342379585980 !ruby/object:Gem::Requirement
49
+ requirement: &70114288904480 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70342379585980
57
+ version_requirements: *70114288904480
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: cucumber
60
- requirement: &70342379581200 !ruby/object:Gem::Requirement
60
+ requirement: &70114288903580 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '1'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70342379581200
68
+ version_requirements: *70114288903580
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
- requirement: &70342379895020 !ruby/object:Gem::Requirement
71
+ requirement: &70114288902160 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '2'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70342379895020
79
+ version_requirements: *70114288902160
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: capybara
82
- requirement: &70342379894540 !ruby/object:Gem::Requirement
82
+ requirement: &70114288901540 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: '1'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70342379894540
90
+ version_requirements: *70114288901540
91
91
  description: Oculus is a web-based logging SQL client. It keeps a history of your
92
92
  queries and the results they returned, so your research is always at hand, easy
93
93
  to share and easy to repeat or reproduce in the future.
@@ -131,6 +131,7 @@ files:
131
131
  - lib/oculus/server/views/index.erb
132
132
  - lib/oculus/server/views/layout.erb
133
133
  - lib/oculus/server/views/show.erb
134
+ - lib/oculus/server/views/starred.erb
134
135
  - lib/oculus/storage.rb
135
136
  - lib/oculus/storage/file_store.rb
136
137
  - lib/oculus/version.rb