tempo-cli 0.1.0

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