tempo-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +56 -0
  4. data/README.md +326 -0
  5. data/Rakefile +65 -0
  6. data/bin/tempo +477 -0
  7. data/features/arrange.feature +43 -0
  8. data/features/checkout.feature +63 -0
  9. data/features/end.feature +65 -0
  10. data/features/project.feature +246 -0
  11. data/features/report.feature +62 -0
  12. data/features/start.feature +87 -0
  13. data/features/step_definitions/tempo_steps.rb +138 -0
  14. data/features/support/env.rb +26 -0
  15. data/features/tempo.feature +13 -0
  16. data/features/update.feature +69 -0
  17. data/lib/file_record/directory.rb +11 -0
  18. data/lib/file_record/directory_structure/tempo/README.txt +4 -0
  19. data/lib/file_record/directory_structure/tempo/tempo_projects.yaml +6 -0
  20. data/lib/file_record/record.rb +120 -0
  21. data/lib/tempo/controllers/arrange_controller.rb +52 -0
  22. data/lib/tempo/controllers/base.rb +117 -0
  23. data/lib/tempo/controllers/checkout_controller.rb +42 -0
  24. data/lib/tempo/controllers/end_controller.rb +42 -0
  25. data/lib/tempo/controllers/projects_controller.rb +107 -0
  26. data/lib/tempo/controllers/records_controller.rb +21 -0
  27. data/lib/tempo/controllers/report_controller.rb +55 -0
  28. data/lib/tempo/controllers/start_controller.rb +42 -0
  29. data/lib/tempo/controllers/update_controller.rb +78 -0
  30. data/lib/tempo/models/base.rb +176 -0
  31. data/lib/tempo/models/composite.rb +71 -0
  32. data/lib/tempo/models/log.rb +194 -0
  33. data/lib/tempo/models/project.rb +73 -0
  34. data/lib/tempo/models/time_record.rb +235 -0
  35. data/lib/tempo/version.rb +3 -0
  36. data/lib/tempo/views/arrange_view.rb +27 -0
  37. data/lib/tempo/views/base.rb +82 -0
  38. data/lib/tempo/views/formatters/base.rb +30 -0
  39. data/lib/tempo/views/formatters/screen.rb +86 -0
  40. data/lib/tempo/views/projects_view.rb +82 -0
  41. data/lib/tempo/views/report_view.rb +26 -0
  42. data/lib/tempo/views/reporter.rb +70 -0
  43. data/lib/tempo/views/time_record_view.rb +30 -0
  44. data/lib/tempo/views/view_records/base.rb +117 -0
  45. data/lib/tempo/views/view_records/composite.rb +40 -0
  46. data/lib/tempo/views/view_records/log.rb +28 -0
  47. data/lib/tempo/views/view_records/project.rb +32 -0
  48. data/lib/tempo/views/view_records/time_record.rb +48 -0
  49. data/lib/tempo.rb +26 -0
  50. data/lib/time_utilities.rb +30 -0
  51. data/tempo-cli.gemspec +26 -0
  52. data/test/lib/file_record/directory_test.rb +30 -0
  53. data/test/lib/file_record/record_test.rb +106 -0
  54. data/test/lib/tempo/controllers/base_controller_test.rb +60 -0
  55. data/test/lib/tempo/controllers/project_controller_test.rb +24 -0
  56. data/test/lib/tempo/models/base_test.rb +173 -0
  57. data/test/lib/tempo/models/composite_test.rb +76 -0
  58. data/test/lib/tempo/models/log_test.rb +171 -0
  59. data/test/lib/tempo/models/project_test.rb +105 -0
  60. data/test/lib/tempo/models/time_record_test.rb +212 -0
  61. data/test/lib/tempo/views/base_test.rb +31 -0
  62. data/test/lib/tempo/views/formatters/base_test.rb +13 -0
  63. data/test/lib/tempo/views/formatters/screen_test.rb +94 -0
  64. data/test/lib/tempo/views/reporter_test.rb +40 -0
  65. data/test/lib/tempo/views/view_records/base_test.rb +77 -0
  66. data/test/lib/tempo/views/view_records/composite_test.rb +57 -0
  67. data/test/lib/tempo/views/view_records/log_test.rb +28 -0
  68. data/test/lib/tempo/views/view_records/project_test.rb +0 -0
  69. data/test/lib/tempo/views/view_records/time_record_test.rb +0 -0
  70. data/test/support/factories.rb +177 -0
  71. data/test/support/helpers.rb +69 -0
  72. data/test/test_helper.rb +31 -0
  73. metadata +230 -0
@@ -0,0 +1,117 @@
1
+ # ViewRecords are simplified models, with additional display information, used in views.
2
+ #
3
+ # Each viewrecord has a :type, which can be queried in the view to know
4
+ # what type of record it is managing.
5
+ #
6
+ # They also each have a format method, which accept a block, and also includes a default
7
+ # block which returns a basic formatted string.
8
+ #
9
+ # ViewRecords can be nested, for instance the Time ViewRecords contain
10
+ # a Duration ViewRecord.
11
+ #
12
+ # View records should add themselves to the Reporter on init, with the exception of
13
+ # partials (such as duration), which are managed within other view records
14
+ #
15
+ # They have no logic, and so it is up
16
+ # to the creation method to make sure they are a correct copy of the information
17
+ # they are representing.
18
+
19
+ module Tempo
20
+ module Views
21
+
22
+ class InvalidViewRecordError < Exception
23
+ end
24
+
25
+ module ViewRecords
26
+
27
+ # The most simple view records, with a message string
28
+ # and a category, which defaults to :info. Categories
29
+ # can be used for color / logging diferentiation.
30
+ # category :error will raise an error after all viewRecords
31
+ # have been run through the reporters
32
+ #
33
+ class Message
34
+ attr_accessor :type, :message, :category
35
+
36
+ def initialize message, options={}
37
+ @message = message
38
+ @category = options.fetch( :category, :info )
39
+ @type = "message"
40
+ Reporter.add_view_record self
41
+ end
42
+
43
+ def format &block
44
+ block ||= lambda {|m| "#{m.message}"}
45
+ block.call self
46
+ end
47
+ end
48
+
49
+ # Specifically for managing a time duration, nested in other
50
+ # view records. This can be used with a start and end time,
51
+ # or used to manage a sum of times.
52
+ #
53
+ # Total duration is stored in seconds.
54
+ #
55
+ # Duration records can be further queried for hours and minutes
56
+ # in order to construct a human redable duration.
57
+ # This can be used to construct time as #{hours}:#{minutes}
58
+ # Hours returns the total whole hours, minutes returns the remaining
59
+ # whole minutes after the hours have been removed from the total.
60
+ #
61
+ class Duration
62
+ attr_accessor :type, :total
63
+
64
+ def initialize seconds=0
65
+ @type = "duration"
66
+ @total = seconds
67
+ end
68
+
69
+ def format &block
70
+ block ||= lambda do |d|
71
+ "#{ d.hours.to_s }:#{ d.minutes.to_s.rjust(2, '0') }"
72
+ end
73
+ block.call self
74
+ end
75
+
76
+ def add seconds
77
+ @total += seconds
78
+ end
79
+
80
+ def subtract seconds
81
+ @total -= seconds
82
+ end
83
+
84
+ def hours
85
+ hours = ( @total / 3600 ).to_i
86
+ end
87
+
88
+ def minutes
89
+ minutes = ( @total / 60 - hours * 60 ).to_i
90
+ end
91
+ end
92
+
93
+ # Base model class, used for extending views for any child of Tempo::Model::Base
94
+ # Sets the id, and type, where type is the class type of the model, for example
95
+ # "project" for Tempo::Model::Project. ViewReord::Model should handle any type of
96
+ # tempo model without error, but most likely won't be as useful as a child class
97
+ # taylored to the specifics of the actual model's child class.
98
+ #
99
+ class Model
100
+ attr_accessor :id, :type
101
+
102
+ def initialize model, options={}
103
+ @id = model.id
104
+
105
+ # example: Tempo::Model::Something => "something"
106
+ @type = /Tempo::Model::(.*)$/.match( model.class.to_s )[1].downcase
107
+ Reporter.add_view_record self
108
+ end
109
+
110
+ def format &block
111
+ block ||= lambda {|model| "#{ model.type.capitalize} #{model.id}"}
112
+ block.call self
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,40 @@
1
+ module Tempo
2
+ module Views
3
+ module ViewRecords
4
+
5
+ # Base Composite model class, used for extending views for any child of Tempo::Model::Composite
6
+ # Inherits the id, and type from ViewRecords::Model, and adds an integer depth to hold the depth
7
+ # within the tree structure of the Composite model. It is important to note, the ViewRecord has
8
+ # no way of determining the depth of the model it represents, and this must be supplied to the
9
+ # instance on instantiation, or after.
10
+ #
11
+ # The Composite ViewRecord class also keeps track of the max depth of all of it's members, this
12
+ # can be used to calculate the padding added to any views.
13
+ #
14
+ # The Composite View Model is an abstract model that is extended to create views for children
15
+ # of the Composite Model class. See ViewRecords::Project for an example.
16
+ #
17
+ class Composite < ViewRecords::Model
18
+ attr_accessor :depth
19
+
20
+ class << self
21
+ def max_depth depth=0
22
+ @max_depth ||= 0
23
+ @max_depth = @max_depth > depth ? @max_depth : depth
24
+ end
25
+ end
26
+
27
+ def initialize model, options={}
28
+ super model, options
29
+ @depth = options.fetch(:depth, 0)
30
+ self.class.max_depth @depth
31
+ end
32
+
33
+ def format &block
34
+ block ||= lambda {|model| "#{" " * model.depth}#{ model.type.capitalize} #{model.id}"}
35
+ block.call self
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module Tempo
2
+ module Views
3
+ module ViewRecords
4
+
5
+ # Base Composite log class, used for extending views for any child of Tempo::Model::Log
6
+ # Inherits the id, and type from ViewRecords::Model, and adds an start time and date_id.
7
+ #
8
+ #
9
+ # The Log View Model is an abstract model that is extended to create views for children
10
+ # of the Log Model class. See ViewRecords::TimeRecord for an example.
11
+ #
12
+ class Log < ViewRecords::Model
13
+ attr_accessor :start_time, :d_id
14
+
15
+ def initialize model, options={}
16
+ super model, options
17
+ @start_time = model.start_time
18
+ @d_id = model.d_id
19
+ end
20
+
21
+ def format &block
22
+ block ||= lambda {|model| "#{ model.type.capitalize} #{model.d_id}-#{model.id} #{model.start_time.strftime('%H:%M')}"}
23
+ block.call self
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ module Tempo
2
+ module Views
3
+ module ViewRecords
4
+
5
+ # Project ViewRecords adds the project title, any tags, and a
6
+ # duration to the composite model. It also keeps track of the
7
+ # maximum title length of all Project views.
8
+ #
9
+ # :depth is inhereted from Composite
10
+ #
11
+ class Project < ViewRecords::Composite
12
+ attr_accessor :title, :tags, :current, :duration
13
+
14
+ class << self
15
+ def max_title_length len=0
16
+ @max_title_length ||= 0
17
+ @max_title_length = @max_title_length > len ? @max_title_length : len
18
+ end
19
+ end
20
+
21
+ def initialize model, options={}
22
+ super model, options
23
+ @title = model.title
24
+ @tags = model.tags
25
+ @current = model.current?
26
+ @duration = Duration.new
27
+ self.class.max_title_length @title.length
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ module Tempo
2
+ module Views
3
+ module ViewRecords
4
+
5
+ # TimeRecord adds a description, project and end_time and running flag
6
+ # to the Log Record to represent any instance of the TimeRecord model.
7
+ # It also includes a Duration ViewRecord for presenting the total duration
8
+ # of the time record.
9
+ #
10
+ class TimeRecord < ViewRecords::Log
11
+ attr_accessor :description, :duration, :end_time, :project, :running
12
+
13
+ class << self
14
+ def max_description_length len=0
15
+ @max_description_length ||= 0
16
+ @max_description_length = @max_description_length > len ? @max_description_length : len
17
+ end
18
+
19
+ def max_project_length len=0
20
+ @max_project_length ||= 0
21
+ @max_project_length = @max_project_length > len ? @max_project_length : len
22
+ end
23
+ end
24
+
25
+ def initialize model, options={}
26
+ super model, options
27
+ @description = model.description
28
+ @description ||= ""
29
+ @duration = Duration.new model.duration
30
+ @end_time = model.end_time == :running ? Time.now() : model.end_time
31
+ @project = model.project_title
32
+ @running = model.running?
33
+ self.class.max_description_length @description.length
34
+ self.class.max_project_length @project.length
35
+ end
36
+
37
+ def format &block
38
+ block ||= lambda do |m|
39
+ running = m.running ? "*" : " "
40
+ description = @description ? "#{m.project}: #{m.description}" : "#{m.project}"
41
+ "#{m.start_time.strftime('%H:%M')} - #{m.end_time.strftime('%H:%M')}#{running} [#{m.duration.format}] #{description}"
42
+ end
43
+ block.call self
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/tempo.rb ADDED
@@ -0,0 +1,26 @@
1
+ # All files required for Tempo to run:
2
+
3
+ # Additional functionality to the time class, requires Chronic:
4
+ require 'time_utilities.rb'
5
+
6
+ require 'tempo/version.rb'
7
+
8
+ require 'tempo/models/base.rb'
9
+ require 'tempo/models/composite.rb'
10
+ require 'tempo/models/log.rb'
11
+ Dir[File.dirname(__FILE__) + '/tempo/models/*.rb'].each {|file| require file }
12
+
13
+ require 'tempo/controllers/base.rb'
14
+ Dir[File.dirname(__FILE__) + '/tempo/controllers/*.rb'].each {|file| require file }
15
+
16
+ Dir[File.dirname(__FILE__) + '/tempo/views/view_records/*.rb'].each {|file| require file }
17
+
18
+ require 'tempo/views/reporter.rb'
19
+ require 'tempo/views/formatters/base.rb'
20
+ Dir[File.dirname(__FILE__) + '/tempo/views/formatters/*.rb'].each {|file| require file }
21
+
22
+ require 'tempo/views/base.rb'
23
+ Dir[File.dirname(__FILE__) + '/tempo/views/*.rb'].each {|file| require file }
24
+
25
+ require 'file_record/directory.rb'
26
+ require 'file_record/record.rb'
@@ -0,0 +1,30 @@
1
+ require 'chronic'
2
+ # see also
3
+ # http://rtmatheson.com/2011/12/rounding-time-to-the-closest-hour-in-ruby/
4
+
5
+ class Time
6
+
7
+ class << self
8
+ def parse time
9
+ # Chronic will usually return nil when unable to parse time
10
+ # it throws an error, on 't' and a few other string, so we
11
+ # capture these here an assure that nil is returned
12
+ begin
13
+ chron = Chronic.parse time
14
+ chron.round
15
+ rescue Exception => e
16
+ return nil
17
+ end
18
+ end
19
+ end
20
+
21
+ #default to whole minutes
22
+ def round options={}
23
+ seconds = 60
24
+ Time.at((self.to_f / seconds).round * seconds)
25
+ end
26
+
27
+ def add_days days
28
+ t = self + days * 86400 # 24 * 60 * 60
29
+ end
30
+ end
data/tempo-cli.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # Ensure we require the local version and not one we might have installed already
2
+ require File.join([File.dirname(__FILE__),'lib','tempo','version.rb'])
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'tempo-cli'
5
+ s.version = Tempo::VERSION
6
+ s.author = 'Jonathan Gabel'
7
+ s.email = 'hello@jonathangabel.com'
8
+ s.homepage = 'http://jonathangabel.com'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'A command line time tracker for recording by day or by project'
11
+ s.description = 'Record and report time spent by project'
12
+ # Add your other files here if you make them
13
+ # Add lib files to lib.tempo.rb
14
+ s.files = `git ls-files`.split("\n")
15
+ s.require_paths << 'lib'
16
+ s.has_rdoc = true
17
+ s.bindir = 'bin'
18
+ s.executables << 'tempo'
19
+ s.add_development_dependency('rake')
20
+ s.add_development_dependency('rdoc')
21
+ s.add_development_dependency('aruba')
22
+ s.add_development_dependency('turn', '~> 0.9.6')
23
+ s.add_development_dependency('pry','~> 0.9.12.2')
24
+ s.add_runtime_dependency('gli','2.6.1')
25
+ s.add_runtime_dependency "chronic", "~> 0.10.2"
26
+ end
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+
3
+ describe FileRecord do
4
+
5
+ before do
6
+ @dir = File.join(Dir.home,"tempo")
7
+ FileUtils.rm_r @dir if File.exists?(@dir)
8
+ end
9
+
10
+ after do
11
+ FileUtils.rm_r @dir if File.exists?(@dir)
12
+ end
13
+
14
+
15
+ describe "Directory" do
16
+
17
+ describe "initialize" do
18
+
19
+ it "should initialize a new directory structure" do
20
+ project_file = File.join(@dir, "tempo_projects.yaml")
21
+ readme = File.join(@dir, "README.txt")
22
+
23
+ FileRecord::Directory.create_new
24
+ File.exists?( @dir ).must_equal true
25
+ File.exists?( project_file ).must_equal true
26
+ File.exists?( readme ).must_equal true
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ describe FileRecord do
4
+
5
+ before do
6
+ # See Rakefile for directory prep and cleanup
7
+ dir = File.join(Dir.home,"tempo")
8
+ Dir.mkdir(dir, 0700) unless File.exists?(dir)
9
+ @dir = File.join(Dir.home,"tempo", "tempo_unit_tests")
10
+ Dir.mkdir(@dir, 0700) unless File.exists?(@dir)
11
+ end
12
+
13
+ describe "Record" do
14
+
15
+ before do
16
+ @file = File.join( @dir, "filerecord_create.txt")
17
+ end
18
+
19
+ after do
20
+ File.delete(@file) if File.exists?(@file)
21
+ end
22
+
23
+ describe "create" do
24
+
25
+ it "should create a new record" do
26
+ FileRecord::Record.create( @file, "" )
27
+ File.exists?( @file ).must_equal true
28
+ end
29
+
30
+ it "should raise and error if the file exists" do
31
+ FileRecord::Record.create( @file, "" )
32
+ proc { FileRecord::Record.create( @file, "" ) }.must_raise ArgumentError
33
+ end
34
+
35
+ it "should overwrite a file with option :force" do
36
+ File.open( @file,'w' ) do |f|
37
+ f.puts "Now this file already exists"
38
+ end
39
+ FileRecord::Record.create( @file, "overwrite file", force: true )
40
+ contents = eval_file_as_array( @file )
41
+ contents.must_equal ["overwrite file"]
42
+ end
43
+ end
44
+
45
+ describe "recording a string" do
46
+ it "should be able to record a string" do
47
+ FileRecord::Record.create( @file, "a simple string" )
48
+ contents = eval_file_as_array( @file )
49
+ contents.must_equal ["a simple string"]
50
+ end
51
+
52
+ it "should be able to record a string as yaml" do
53
+ FileRecord::Record.create( @file, "a simple string", format: 'yaml' )
54
+ contents = eval_file_as_array( @file )
55
+ contents.must_equal ["--- a simple string", "..."]
56
+ end
57
+ end
58
+
59
+ describe "recording and array" do
60
+
61
+ it "should be able to record a shallow array as string" do
62
+ FileRecord::Record.create( @file, ["a","simple","array"], format: "string" )
63
+ contents = eval_file_as_array( @file )
64
+ contents.must_equal ["a","simple","array"]
65
+ end
66
+
67
+ it "should default to recording a shallow array as yaml" do
68
+ FileRecord::Record.create( @file, ["a","simple","array"] )
69
+ contents = eval_file_as_array( @file )
70
+ contents.must_equal ["---", "- a", "- simple", "- array"]
71
+ end
72
+
73
+ it "should record a nested array as yaml" do
74
+ FileRecord::Record.create( @file, ["a",["nested",["array"]]])
75
+ contents = eval_file_as_array( @file )
76
+ contents.must_equal ["---", "- a", "- - nested", " - - array"]
77
+ end
78
+ end
79
+
80
+ describe "recording a hash" do
81
+
82
+ it "should default to and record a hash as yaml" do
83
+ hash = {a: 1, b: true, c: Hash.new, d: "object", with: ['an', 'array']}
84
+ FileRecord::Record.create( @file, hash )
85
+ contents = eval_file_as_array( @file )
86
+ contents.must_equal ["---", ":a: 1", ":b: true", ":c: {}", ":d: object", ":with:", "- an", "- array"]
87
+ end
88
+ end
89
+
90
+ describe "recording a Tempo Model" do
91
+
92
+ it "should create a record of all instances" do
93
+ test_file = File.join(ENV['HOME'],'tempo','tempo_animals.yaml')
94
+ File.delete( test_file ) if File.exists?( test_file )
95
+ pantherinae_factory
96
+ FileRecord::Record.save_model( Tempo::Model::Animal )
97
+ contents = eval_file_as_array( test_file )
98
+ contents.must_equal [ "---", ":id: 1", ":genious: Panthera", ":species: p. tigris",
99
+ "---", ":id: 2", ":genious: Panthera", ":species: p. leo",
100
+ "---", ":id: 3", ":genious: Panthera", ":species: p. onca",
101
+ "---", ":id: 4", ":genious: Panthera", ":species: p. pardus",
102
+ "---", ":id: 5", ":genious: Panthera", ":species: p. zdanskyi"]
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,60 @@
1
+ require "test_helper"
2
+
3
+ describe Tempo::Controllers::Base do
4
+ describe "Class Methods" do
5
+
6
+ before do
7
+ @controller = Tempo::Controllers::Base
8
+ end
9
+
10
+ describe "fuzzy match" do
11
+ describe "matching an array" do
12
+
13
+ before do
14
+ @haystack = ['sheep','ducks','peeping-ducks','sheep ducks']
15
+ end
16
+
17
+ it "should find all matches to a single match object" do
18
+ @controller.fuzzy_match( @haystack, 'ducks' ).must_equal ['ducks','peeping-ducks','sheep ducks']
19
+ end
20
+
21
+ it "should find all matches to an array of match objects" do
22
+ @controller.fuzzy_match( @haystack, ['ducks', 'eep'] ).must_equal ['peeping-ducks','sheep ducks']
23
+ end
24
+ end
25
+
26
+ describe "matching a Tempo Model" do
27
+
28
+ before do
29
+ pantherinae_factory
30
+ end
31
+
32
+ it "should find all matches to a single match object" do
33
+ matches = @controller.fuzzy_match( Tempo::Model::Animal, "Panthera", "genious" )
34
+ matches.length.must_equal 5
35
+ end
36
+
37
+ it "should find all matches to an array of match objects" do
38
+ matches = @controller.fuzzy_match( Tempo::Model::Animal, ["p. ", "a"], "species" )
39
+ # "p. onca", "p. pardus" "p. zdanskyi"
40
+ matches.length.must_equal 3
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "reassemble to args" do
46
+
47
+ it "should reassemble the args passed in as an array" do
48
+ test_args = ["an", "array", "of", "args"]
49
+ test_flag = "I'm"
50
+ @controller.reassemble_the( test_args ).must_equal "an array of args"
51
+ end
52
+
53
+ it "should reassemble the args with a flag in the front" do
54
+ test_args = ["an", "array", "of", "args"]
55
+ test_flag = "I'm"
56
+ @controller.reassemble_the( test_args, test_flag ).must_equal "I'm an array of args"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ require "test_helper"
2
+
3
+ describe Tempo::Controllers::Projects do
4
+ describe "Class Methods" do
5
+
6
+ before do
7
+ @controller = Tempo::Controllers::Projects
8
+ end
9
+
10
+ describe "exact match" do
11
+
12
+ before do
13
+ project_factory
14
+ Tempo::Model::Project.new title: 'horticulture'
15
+ end
16
+
17
+ it "should find an exact match" do
18
+ match1 = @controller.filter_projects_by_title({ exact: true }, ["horticulture"])
19
+ match1.length.must_equal 1
20
+ match1[0].title.must_equal "horticulture"
21
+ end
22
+ end
23
+ end
24
+ end