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.
- data/.gitignore +4 -0
 - data/Gemfile +4 -0
 - data/Gemfile.lock +56 -0
 - data/README.md +326 -0
 - data/Rakefile +65 -0
 - data/bin/tempo +477 -0
 - data/features/arrange.feature +43 -0
 - data/features/checkout.feature +63 -0
 - data/features/end.feature +65 -0
 - data/features/project.feature +246 -0
 - data/features/report.feature +62 -0
 - data/features/start.feature +87 -0
 - data/features/step_definitions/tempo_steps.rb +138 -0
 - data/features/support/env.rb +26 -0
 - data/features/tempo.feature +13 -0
 - data/features/update.feature +69 -0
 - data/lib/file_record/directory.rb +11 -0
 - data/lib/file_record/directory_structure/tempo/README.txt +4 -0
 - data/lib/file_record/directory_structure/tempo/tempo_projects.yaml +6 -0
 - data/lib/file_record/record.rb +120 -0
 - data/lib/tempo/controllers/arrange_controller.rb +52 -0
 - data/lib/tempo/controllers/base.rb +117 -0
 - data/lib/tempo/controllers/checkout_controller.rb +42 -0
 - data/lib/tempo/controllers/end_controller.rb +42 -0
 - data/lib/tempo/controllers/projects_controller.rb +107 -0
 - data/lib/tempo/controllers/records_controller.rb +21 -0
 - data/lib/tempo/controllers/report_controller.rb +55 -0
 - data/lib/tempo/controllers/start_controller.rb +42 -0
 - data/lib/tempo/controllers/update_controller.rb +78 -0
 - data/lib/tempo/models/base.rb +176 -0
 - data/lib/tempo/models/composite.rb +71 -0
 - data/lib/tempo/models/log.rb +194 -0
 - data/lib/tempo/models/project.rb +73 -0
 - data/lib/tempo/models/time_record.rb +235 -0
 - data/lib/tempo/version.rb +3 -0
 - data/lib/tempo/views/arrange_view.rb +27 -0
 - data/lib/tempo/views/base.rb +82 -0
 - data/lib/tempo/views/formatters/base.rb +30 -0
 - data/lib/tempo/views/formatters/screen.rb +86 -0
 - data/lib/tempo/views/projects_view.rb +82 -0
 - data/lib/tempo/views/report_view.rb +26 -0
 - data/lib/tempo/views/reporter.rb +70 -0
 - data/lib/tempo/views/time_record_view.rb +30 -0
 - data/lib/tempo/views/view_records/base.rb +117 -0
 - data/lib/tempo/views/view_records/composite.rb +40 -0
 - data/lib/tempo/views/view_records/log.rb +28 -0
 - data/lib/tempo/views/view_records/project.rb +32 -0
 - data/lib/tempo/views/view_records/time_record.rb +48 -0
 - data/lib/tempo.rb +26 -0
 - data/lib/time_utilities.rb +30 -0
 - data/tempo-cli.gemspec +26 -0
 - data/test/lib/file_record/directory_test.rb +30 -0
 - data/test/lib/file_record/record_test.rb +106 -0
 - data/test/lib/tempo/controllers/base_controller_test.rb +60 -0
 - data/test/lib/tempo/controllers/project_controller_test.rb +24 -0
 - data/test/lib/tempo/models/base_test.rb +173 -0
 - data/test/lib/tempo/models/composite_test.rb +76 -0
 - data/test/lib/tempo/models/log_test.rb +171 -0
 - data/test/lib/tempo/models/project_test.rb +105 -0
 - data/test/lib/tempo/models/time_record_test.rb +212 -0
 - data/test/lib/tempo/views/base_test.rb +31 -0
 - data/test/lib/tempo/views/formatters/base_test.rb +13 -0
 - data/test/lib/tempo/views/formatters/screen_test.rb +94 -0
 - data/test/lib/tempo/views/reporter_test.rb +40 -0
 - data/test/lib/tempo/views/view_records/base_test.rb +77 -0
 - data/test/lib/tempo/views/view_records/composite_test.rb +57 -0
 - data/test/lib/tempo/views/view_records/log_test.rb +28 -0
 - data/test/lib/tempo/views/view_records/project_test.rb +0 -0
 - data/test/lib/tempo/views/view_records/time_record_test.rb +0 -0
 - data/test/support/factories.rb +177 -0
 - data/test/support/helpers.rb +69 -0
 - data/test/test_helper.rb +31 -0
 - metadata +230 -0
 
| 
         @@ -0,0 +1,176 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Tempo
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
                class IdentityConflictError < Exception
         
     | 
| 
      
 5 
     | 
    
         
            +
                end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                class Base
         
     | 
| 
      
 8 
     | 
    
         
            +
                  attr_reader :id
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    # Maintain an array of unique ids for the class.
         
     | 
| 
      
 13 
     | 
    
         
            +
                    # Initialize new members with the next numberical id
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # Ids can be assigned on init (for the purpose of reading
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # in records of previous instances). An error will
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # be raised if there is already an instance with that
         
     | 
| 
      
 17 
     | 
    
         
            +
                    # id.
         
     | 
| 
      
 18 
     | 
    
         
            +
                    def id_counter
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @id_counter ||= 1
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    def ids
         
     | 
| 
      
 23 
     | 
    
         
            +
                      @ids ||= []
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    def index
         
     | 
| 
      
 27 
     | 
    
         
            +
                      @index ||= []
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    # example: Tempo::Model::Animal -> tempo_animals.yaml
         
     | 
| 
      
 31 
     | 
    
         
            +
                    def file
         
     | 
| 
      
 32 
     | 
    
         
            +
                      FileRecord::Record.model_filename self
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    def save_to_file
         
     | 
| 
      
 36 
     | 
    
         
            +
                      FileRecord::Record.save_model self
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    def read_from_file
         
     | 
| 
      
 40 
     | 
    
         
            +
                      FileRecord::Record.read_model self
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    def method_missing meth, *args, &block
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                      if meth.to_s =~ /^find_by_(.+)$/
         
     | 
| 
      
 46 
     | 
    
         
            +
                        run_find_by_method($1, *args, &block)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      elsif meth.to_s =~ /^sort_by_(.+)$/
         
     | 
| 
      
 49 
     | 
    
         
            +
                        run_sort_by_method($1, *args, &block)
         
     | 
| 
      
 50 
     | 
    
         
            +
                      else
         
     | 
| 
      
 51 
     | 
    
         
            +
                        super
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end
         
     | 
| 
      
 53 
     | 
    
         
            +
                    end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    def run_sort_by_method attribute, args=@index.clone, &block
         
     | 
| 
      
 56 
     | 
    
         
            +
                      attr = "@#{attribute}".to_sym
         
     | 
| 
      
 57 
     | 
    
         
            +
                      args.sort! { |a,b| a.instance_variable_get( attr ) <=> b.instance_variable_get( attr ) }
         
     | 
| 
      
 58 
     | 
    
         
            +
                      return args unless block
         
     | 
| 
      
 59 
     | 
    
         
            +
                      block.call args
         
     | 
| 
      
 60 
     | 
    
         
            +
                    end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                    def run_find_by_method attrs, *args, &block
         
     | 
| 
      
 63 
     | 
    
         
            +
                      # Make an array of attribute names
         
     | 
| 
      
 64 
     | 
    
         
            +
                      attrs = attrs.split('_and_')
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                      attrs_with_args = [attrs, args].transpose
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                      filtered = index.clone
         
     | 
| 
      
 69 
     | 
    
         
            +
                      attrs_with_args.each do | kv |
         
     | 
| 
      
 70 
     | 
    
         
            +
                        matches = find kv[0], kv[1]
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                        return matches if matches.empty?
         
     | 
| 
      
 73 
     | 
    
         
            +
                        matches.each do |match|
         
     | 
| 
      
 74 
     | 
    
         
            +
                          matches.delete match unless filtered.include? match
         
     | 
| 
      
 75 
     | 
    
         
            +
                          filtered = matches
         
     | 
| 
      
 76 
     | 
    
         
            +
                        end
         
     | 
| 
      
 77 
     | 
    
         
            +
                      end
         
     | 
| 
      
 78 
     | 
    
         
            +
                      filtered
         
     | 
| 
      
 79 
     | 
    
         
            +
                    end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    # find by id should be exact, so we remove the array wrapper
         
     | 
| 
      
 82 
     | 
    
         
            +
                    def find_by_id id
         
     | 
| 
      
 83 
     | 
    
         
            +
                      matches = find "id", id
         
     | 
| 
      
 84 
     | 
    
         
            +
                      match = matches[0]
         
     | 
| 
      
 85 
     | 
    
         
            +
                    end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    # example: Tempo::Model.find("id", 1)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    #
         
     | 
| 
      
 89 
     | 
    
         
            +
                    def find key, value
         
     | 
| 
      
 90 
     | 
    
         
            +
                      key = "@#{key}".to_sym
         
     | 
| 
      
 91 
     | 
    
         
            +
                      matches = []
         
     | 
| 
      
 92 
     | 
    
         
            +
                      index.each do |i|
         
     | 
| 
      
 93 
     | 
    
         
            +
                        stored_value = i.instance_variable_get( key )
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                        if stored_value.kind_of? String
         
     | 
| 
      
 96 
     | 
    
         
            +
                          if value.kind_of? Regexp
         
     | 
| 
      
 97 
     | 
    
         
            +
                            matches << i if value.match stored_value
         
     | 
| 
      
 98 
     | 
    
         
            +
                          else
         
     | 
| 
      
 99 
     | 
    
         
            +
                            matches << i if stored_value.downcase.include? value.to_s.downcase
         
     | 
| 
      
 100 
     | 
    
         
            +
                          end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                        elsif stored_value.kind_of? Integer
         
     | 
| 
      
 103 
     | 
    
         
            +
                          matches << i if stored_value == value.to_i
         
     | 
| 
      
 104 
     | 
    
         
            +
                        end
         
     | 
| 
      
 105 
     | 
    
         
            +
                      end
         
     | 
| 
      
 106 
     | 
    
         
            +
                      matches
         
     | 
| 
      
 107 
     | 
    
         
            +
                    end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                    def delete instance
         
     | 
| 
      
 110 
     | 
    
         
            +
                      id = instance.id
         
     | 
| 
      
 111 
     | 
    
         
            +
                      index.delete( instance )
         
     | 
| 
      
 112 
     | 
    
         
            +
                      ids.delete( id )
         
     | 
| 
      
 113 
     | 
    
         
            +
                    end
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  def initialize options={}
         
     | 
| 
      
 117 
     | 
    
         
            +
                    id_candidate = options[:id]
         
     | 
| 
      
 118 
     | 
    
         
            +
                    if !id_candidate
         
     | 
| 
      
 119 
     | 
    
         
            +
                      @id = self.class.next_id
         
     | 
| 
      
 120 
     | 
    
         
            +
                    elsif self.class.ids.include? id_candidate
         
     | 
| 
      
 121 
     | 
    
         
            +
                      raise IdentityConflictError, "Id #{id_candidate} already exists"
         
     | 
| 
      
 122 
     | 
    
         
            +
                    else
         
     | 
| 
      
 123 
     | 
    
         
            +
                      @id = id_candidate
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
                    self.class.add_id @id
         
     | 
| 
      
 126 
     | 
    
         
            +
                    self.class.add_to_index self
         
     | 
| 
      
 127 
     | 
    
         
            +
                  end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                  # record the state of all instance variables as a hash
         
     | 
| 
      
 130 
     | 
    
         
            +
                  def freeze_dry
         
     | 
| 
      
 131 
     | 
    
         
            +
                    record = {}
         
     | 
| 
      
 132 
     | 
    
         
            +
                    state = instance_variables
         
     | 
| 
      
 133 
     | 
    
         
            +
                    state.each do |attr|
         
     | 
| 
      
 134 
     | 
    
         
            +
                      key = attr[1..-1].to_sym
         
     | 
| 
      
 135 
     | 
    
         
            +
                      val = instance_variable_get attr
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                      #val = val.to_s if val.kind_of? Time
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
                      record[key] = val
         
     | 
| 
      
 140 
     | 
    
         
            +
                    end
         
     | 
| 
      
 141 
     | 
    
         
            +
                    record
         
     | 
| 
      
 142 
     | 
    
         
            +
                  end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                  def delete
         
     | 
| 
      
 145 
     | 
    
         
            +
                    self.class.delete self
         
     | 
| 
      
 146 
     | 
    
         
            +
                  end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                  protected
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                  def self.add_to_index member
         
     | 
| 
      
 151 
     | 
    
         
            +
                    @index ||= []
         
     | 
| 
      
 152 
     | 
    
         
            +
                    @index << member
         
     | 
| 
      
 153 
     | 
    
         
            +
                    @index.sort! { |a,b| a.id <=> b.id }
         
     | 
| 
      
 154 
     | 
    
         
            +
                  end
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  def self.add_id id
         
     | 
| 
      
 157 
     | 
    
         
            +
                    @ids ||=[]
         
     | 
| 
      
 158 
     | 
    
         
            +
                    @ids << id
         
     | 
| 
      
 159 
     | 
    
         
            +
                    @ids.sort!
         
     | 
| 
      
 160 
     | 
    
         
            +
                  end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                  def self.increase_id_counter
         
     | 
| 
      
 163 
     | 
    
         
            +
                    @id_counter ||= 0
         
     | 
| 
      
 164 
     | 
    
         
            +
                    @id_counter = @id_counter.next
         
     | 
| 
      
 165 
     | 
    
         
            +
                  end
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                  def self.next_id
         
     | 
| 
      
 168 
     | 
    
         
            +
                    while ids.include? id_counter
         
     | 
| 
      
 169 
     | 
    
         
            +
                      increase_id_counter
         
     | 
| 
      
 170 
     | 
    
         
            +
                    end
         
     | 
| 
      
 171 
     | 
    
         
            +
                    id_counter
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
      
 173 
     | 
    
         
            +
                end
         
     | 
| 
      
 174 
     | 
    
         
            +
              end
         
     | 
| 
      
 175 
     | 
    
         
            +
            end
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,71 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Composite Model extends base to accomodate tree structures
         
     | 
| 
      
 2 
     | 
    
         
            +
            # Each instance can be a root instance, or a child of another
         
     | 
| 
      
 3 
     | 
    
         
            +
            # instance, and each instance can have any number of children.
         
     | 
| 
      
 4 
     | 
    
         
            +
            # report_trees is a utility method for testing the validity of the
         
     | 
| 
      
 5 
     | 
    
         
            +
            # model, and cam be used as a template for creating tree reports.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Tempo
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 9 
     | 
    
         
            +
                class Composite < Tempo::Model::Base
         
     | 
| 
      
 10 
     | 
    
         
            +
                  attr_accessor :parent, :children
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    def report_trees
         
     | 
| 
      
 15 
     | 
    
         
            +
                      report_array = "["
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @index.each do |member|
         
     | 
| 
      
 17 
     | 
    
         
            +
                        if member.parent == :root
         
     | 
| 
      
 18 
     | 
    
         
            +
                          report_array += "["
         
     | 
| 
      
 19 
     | 
    
         
            +
                          report_array += member.report_branches
         
     | 
| 
      
 20 
     | 
    
         
            +
                          report_array += "],"
         
     | 
| 
      
 21 
     | 
    
         
            +
                        end
         
     | 
| 
      
 22 
     | 
    
         
            +
                      end
         
     | 
| 
      
 23 
     | 
    
         
            +
                      if report_array[-1] == ","
         
     | 
| 
      
 24 
     | 
    
         
            +
                        report_array = report_array[0..-2]
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
                      report_array += "]"
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    def delete instance
         
     | 
| 
      
 30 
     | 
    
         
            +
                      instance.children.each do |child_id|
         
     | 
| 
      
 31 
     | 
    
         
            +
                        child = find_by_id child_id
         
     | 
| 
      
 32 
     | 
    
         
            +
                        instance.remove_child child
         
     | 
| 
      
 33 
     | 
    
         
            +
                      end
         
     | 
| 
      
 34 
     | 
    
         
            +
                      super instance
         
     | 
| 
      
 35 
     | 
    
         
            +
                    end
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  def initialize(options={})
         
     | 
| 
      
 39 
     | 
    
         
            +
                    super options
         
     | 
| 
      
 40 
     | 
    
         
            +
                    @parent = options.fetch(:parent, :root)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    @children = options.fetch(:children, [])
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  def << child
         
     | 
| 
      
 45 
     | 
    
         
            +
                    @children << child.id unless @children.include? child.id
         
     | 
| 
      
 46 
     | 
    
         
            +
                    @children.sort!
         
     | 
| 
      
 47 
     | 
    
         
            +
                    child.parent = self.id
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  def remove_child( child )
         
     | 
| 
      
 51 
     | 
    
         
            +
                    @children.delete child.id
         
     | 
| 
      
 52 
     | 
    
         
            +
                    child.parent = :root
         
     | 
| 
      
 53 
     | 
    
         
            +
                  end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  def report_branches
         
     | 
| 
      
 56 
     | 
    
         
            +
                    report = self.id.to_s
         
     | 
| 
      
 57 
     | 
    
         
            +
                    child_report = ",["
         
     | 
| 
      
 58 
     | 
    
         
            +
                    @children.each do |c|
         
     | 
| 
      
 59 
     | 
    
         
            +
                      child = self.class.find_by_id c
         
     | 
| 
      
 60 
     | 
    
         
            +
                      child_report += "#{child.report_branches},"
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
                    if child_report == ",["
         
     | 
| 
      
 63 
     | 
    
         
            +
                      child_report = ""
         
     | 
| 
      
 64 
     | 
    
         
            +
                    else
         
     | 
| 
      
 65 
     | 
    
         
            +
                      child_report = child_report[0..-2] + "]"
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
                    report += child_report
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
              end
         
     | 
| 
      
 71 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,194 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Tempo
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Log < Tempo::Model::Base
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :start_time
         
     | 
| 
      
 5 
     | 
    
         
            +
                  attr_reader :d_id
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    # Maintain arrays of unique ids for each day.
         
     | 
| 
      
 10 
     | 
    
         
            +
                    # days are represented as symbols in the hash,
         
     | 
| 
      
 11 
     | 
    
         
            +
                    # for example Jan 1, 2013 would be  :"130101"
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # id counter is managed through the private methods
         
     | 
| 
      
 13 
     | 
    
         
            +
                    # increase_id_counter and next_id below
         
     | 
| 
      
 14 
     | 
    
         
            +
                    def id_counter time
         
     | 
| 
      
 15 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @id_counter = {} unless @id_counter.kind_of? Hash
         
     | 
| 
      
 17 
     | 
    
         
            +
                      @id_counter[ dsym ] ||= 1
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    def ids time
         
     | 
| 
      
 21 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 22 
     | 
    
         
            +
                      @ids = {} unless @ids.kind_of? Hash
         
     | 
| 
      
 23 
     | 
    
         
            +
                      @ids[dsym] ||= []
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    # all instances are saved in the index inherited from base.
         
     | 
| 
      
 27 
     | 
    
         
            +
                    # Additionally, the days index organizes all instances into
         
     | 
| 
      
 28 
     | 
    
         
            +
                    # arrays by day.  This is used for saving to file.
         
     | 
| 
      
 29 
     | 
    
         
            +
                    def days_index
         
     | 
| 
      
 30 
     | 
    
         
            +
                      @days_index = {} unless @days_index.kind_of? Hash
         
     | 
| 
      
 31 
     | 
    
         
            +
                      @days_index
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    def file time
         
     | 
| 
      
 35 
     | 
    
         
            +
                      FileRecord::Record.log_filename( self, time )
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    def dir
         
     | 
| 
      
 39 
     | 
    
         
            +
                      FileRecord::Record.log_dirname( self )
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    def records
         
     | 
| 
      
 43 
     | 
    
         
            +
                      path = FileRecord::Record.log_dir( self )
         
     | 
| 
      
 44 
     | 
    
         
            +
                      Dir[path + "/*.yaml"]
         
     | 
| 
      
 45 
     | 
    
         
            +
                    end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                    def save_to_file
         
     | 
| 
      
 48 
     | 
    
         
            +
                      FileRecord::Record.save_log( self )
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    def read_from_file time
         
     | 
| 
      
 53 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 54 
     | 
    
         
            +
                      @days_index[ dsym ] = [] if not days_index.has_key? dsym
         
     | 
| 
      
 55 
     | 
    
         
            +
                      FileRecord::Record.read_log( self, time )
         
     | 
| 
      
 56 
     | 
    
         
            +
                    end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    # load all the records for a single day
         
     | 
| 
      
 59 
     | 
    
         
            +
                    def load_day_record time
         
     | 
| 
      
 60 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 61 
     | 
    
         
            +
                      if not days_index.has_key? dsym
         
     | 
| 
      
 62 
     | 
    
         
            +
                        @days_index[ dsym ] = []
         
     | 
| 
      
 63 
     | 
    
         
            +
                        read_from_file time
         
     | 
| 
      
 64 
     | 
    
         
            +
                      end
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                    # load the records for each day from time 1 to time 2
         
     | 
| 
      
 68 
     | 
    
         
            +
                    def load_days_records time_1, time_2
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                      return if time_1.nil? || time_2.nil?
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      days = ( time_2.to_date - time_1.to_date ).to_i
         
     | 
| 
      
 73 
     | 
    
         
            +
                      return if days < 0
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                      (days + 1).times { |i| load_day_record( time_1.add_days( i ))}
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    # load the records for the most recently recorded day
         
     | 
| 
      
 79 
     | 
    
         
            +
                    def load_last_day
         
     | 
| 
      
 80 
     | 
    
         
            +
                      reg = /(\d+)\.yaml/
         
     | 
| 
      
 81 
     | 
    
         
            +
                      if records.last
         
     | 
| 
      
 82 
     | 
    
         
            +
                        d_id = reg.match(records.last)[1] if records.last
         
     | 
| 
      
 83 
     | 
    
         
            +
                        time = day_id_to_time d_id if d_id
         
     | 
| 
      
 84 
     | 
    
         
            +
                        load_day_record time
         
     | 
| 
      
 85 
     | 
    
         
            +
                        return time
         
     | 
| 
      
 86 
     | 
    
         
            +
                      end
         
     | 
| 
      
 87 
     | 
    
         
            +
                    end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    # takes and integer, and time or day_id
         
     | 
| 
      
 90 
     | 
    
         
            +
                    # and returns the instance that matches both
         
     | 
| 
      
 91 
     | 
    
         
            +
                    # the id and d_id
         
     | 
| 
      
 92 
     | 
    
         
            +
                    def find_by_id id, time
         
     | 
| 
      
 93 
     | 
    
         
            +
                      time = day_id time
         
     | 
| 
      
 94 
     | 
    
         
            +
                      ids = find "id", id
         
     | 
| 
      
 95 
     | 
    
         
            +
                      d_ids = find "d_id", time
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                      #return the first and only match in the union
         
     | 
| 
      
 98 
     | 
    
         
            +
                      #of the arrays
         
     | 
| 
      
 99 
     | 
    
         
            +
                      (ids & d_ids)[0]
         
     | 
| 
      
 100 
     | 
    
         
            +
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    # day_ids can be run through without change
         
     | 
| 
      
 103 
     | 
    
         
            +
                    # Time will be converted into "YYYYmmdd"
         
     | 
| 
      
 104 
     | 
    
         
            +
                    # ex: 1-1-2014 => "20140101"
         
     | 
| 
      
 105 
     | 
    
         
            +
                    def day_id time
         
     | 
| 
      
 106 
     | 
    
         
            +
                      if time.kind_of? String
         
     | 
| 
      
 107 
     | 
    
         
            +
                        return time if time =~ /^\d{8}$/
         
     | 
| 
      
 108 
     | 
    
         
            +
                      end
         
     | 
| 
      
 109 
     | 
    
         
            +
                      raise ArgumentError, "Invalid Time" if not time.kind_of? Time
         
     | 
| 
      
 110 
     | 
    
         
            +
                      time.strftime("%Y%m%d")
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    def day_id_to_time d_id
         
     | 
| 
      
 114 
     | 
    
         
            +
                      time = Time.new(d_id[0..3].to_i, d_id[4..5].to_i, d_id[6..7].to_i)
         
     | 
| 
      
 115 
     | 
    
         
            +
                    end
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                    def delete instance
         
     | 
| 
      
 118 
     | 
    
         
            +
                      id = instance.id
         
     | 
| 
      
 119 
     | 
    
         
            +
                      dsym = date_symbol instance.d_id
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                      index.delete instance
         
     | 
| 
      
 122 
     | 
    
         
            +
                      days_index[dsym].delete instance
         
     | 
| 
      
 123 
     | 
    
         
            +
                      @ids[dsym].delete id
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
                  end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                  def initialize( options={} )
         
     | 
| 
      
 128 
     | 
    
         
            +
                    @start_time = options.fetch(:start_time, Time.now )
         
     | 
| 
      
 129 
     | 
    
         
            +
                    @start_time = Time.new(@start_time) if @start_time.kind_of? String
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                    self.class.load_day_record(@start_time)
         
     | 
| 
      
 132 
     | 
    
         
            +
                    @d_id = self.class.day_id @start_time
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                    id_candidate = options[:id]
         
     | 
| 
      
 135 
     | 
    
         
            +
                    if !id_candidate
         
     | 
| 
      
 136 
     | 
    
         
            +
                      @id = self.class.next_id @start_time
         
     | 
| 
      
 137 
     | 
    
         
            +
                    elsif self.class.ids( @start_time ).include? id_candidate
         
     | 
| 
      
 138 
     | 
    
         
            +
                      raise IdentityConflictError, "Id #{id_candidate} already exists"
         
     | 
| 
      
 139 
     | 
    
         
            +
                    else
         
     | 
| 
      
 140 
     | 
    
         
            +
                      @id = id_candidate
         
     | 
| 
      
 141 
     | 
    
         
            +
                    end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                    self.class.add_id @start_time, @id
         
     | 
| 
      
 144 
     | 
    
         
            +
                    self.class.add_to_index self
         
     | 
| 
      
 145 
     | 
    
         
            +
                    self.class.add_to_days_index self
         
     | 
| 
      
 146 
     | 
    
         
            +
                  end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                  def freeze_dry
         
     | 
| 
      
 149 
     | 
    
         
            +
                    record = super
         
     | 
| 
      
 150 
     | 
    
         
            +
                    record.delete(:d_id)
         
     | 
| 
      
 151 
     | 
    
         
            +
                    record
         
     | 
| 
      
 152 
     | 
    
         
            +
                  end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                  protected
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                    def add_to_days_index member
         
     | 
| 
      
 159 
     | 
    
         
            +
                      @days_index = {} unless @days_index.kind_of? Hash
         
     | 
| 
      
 160 
     | 
    
         
            +
                      dsym = date_symbol member.start_time
         
     | 
| 
      
 161 
     | 
    
         
            +
                      @days_index[dsym] ||= []
         
     | 
| 
      
 162 
     | 
    
         
            +
                      @days_index[dsym] << member
         
     | 
| 
      
 163 
     | 
    
         
            +
                      @days_index[dsym].sort! { |a,b| a.start_time <=> b.start_time }
         
     | 
| 
      
 164 
     | 
    
         
            +
                    end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                    def add_id time, id
         
     | 
| 
      
 167 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 168 
     | 
    
         
            +
                      @ids = {} unless @ids.kind_of? Hash
         
     | 
| 
      
 169 
     | 
    
         
            +
                      @ids[dsym] ||= []
         
     | 
| 
      
 170 
     | 
    
         
            +
                      @ids[dsym] << id
         
     | 
| 
      
 171 
     | 
    
         
            +
                      @ids[dsym].sort!
         
     | 
| 
      
 172 
     | 
    
         
            +
                    end
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                    def date_symbol time
         
     | 
| 
      
 175 
     | 
    
         
            +
                      day_id( time ).to_sym
         
     | 
| 
      
 176 
     | 
    
         
            +
                    end
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                    def increase_id_counter time
         
     | 
| 
      
 179 
     | 
    
         
            +
                      dsym = date_symbol time
         
     | 
| 
      
 180 
     | 
    
         
            +
                      @id_counter = {} unless @id_counter.kind_of? Hash
         
     | 
| 
      
 181 
     | 
    
         
            +
                      @id_counter[ dsym ] ||= 0
         
     | 
| 
      
 182 
     | 
    
         
            +
                      @id_counter[ dsym ] = @id_counter[ dsym ].next
         
     | 
| 
      
 183 
     | 
    
         
            +
                    end
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                    def next_id time
         
     | 
| 
      
 186 
     | 
    
         
            +
                      while ids(time).include? id_counter time
         
     | 
| 
      
 187 
     | 
    
         
            +
                        increase_id_counter time
         
     | 
| 
      
 188 
     | 
    
         
            +
                      end
         
     | 
| 
      
 189 
     | 
    
         
            +
                      id_counter time
         
     | 
| 
      
 190 
     | 
    
         
            +
                    end
         
     | 
| 
      
 191 
     | 
    
         
            +
                  end
         
     | 
| 
      
 192 
     | 
    
         
            +
                end
         
     | 
| 
      
 193 
     | 
    
         
            +
              end
         
     | 
| 
      
 194 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,73 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Tempo
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Project < Tempo::Model::Composite
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :title
         
     | 
| 
      
 5 
     | 
    
         
            +
                  attr_reader :tags
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @current = 0
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                    def current( instance=nil )
         
     | 
| 
      
 11 
     | 
    
         
            +
                      @current
         
     | 
| 
      
 12 
     | 
    
         
            +
                    end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    def current=( instance )
         
     | 
| 
      
 15 
     | 
    
         
            +
                      if instance.class == self
         
     | 
| 
      
 16 
     | 
    
         
            +
                        @current = instance
         
     | 
| 
      
 17 
     | 
    
         
            +
                      else
         
     | 
| 
      
 18 
     | 
    
         
            +
                        raise ArgumentError
         
     | 
| 
      
 19 
     | 
    
         
            +
                      end
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    def include?( title )
         
     | 
| 
      
 23 
     | 
    
         
            +
                      matches = find_by_title( title )
         
     | 
| 
      
 24 
     | 
    
         
            +
                      return false if matches.empty?
         
     | 
| 
      
 25 
     | 
    
         
            +
                      matches.each do |match|
         
     | 
| 
      
 26 
     | 
    
         
            +
                        return true if match.title == title
         
     | 
| 
      
 27 
     | 
    
         
            +
                      end
         
     | 
| 
      
 28 
     | 
    
         
            +
                      false
         
     | 
| 
      
 29 
     | 
    
         
            +
                    end
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  def initialize(options={})
         
     | 
| 
      
 33 
     | 
    
         
            +
                    super options
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @title = options.fetch(:title, "new project")
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @tags = []
         
     | 
| 
      
 36 
     | 
    
         
            +
                    tag options.fetch(:tags, [])
         
     | 
| 
      
 37 
     | 
    
         
            +
                    current = options.fetch(:current, false)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    self.class.current = self if current
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def current?
         
     | 
| 
      
 42 
     | 
    
         
            +
                    self.class.current == self
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def freeze_dry
         
     | 
| 
      
 46 
     | 
    
         
            +
                    record = super
         
     | 
| 
      
 47 
     | 
    
         
            +
                    if self.class.current == self
         
     | 
| 
      
 48 
     | 
    
         
            +
                      record[:current] = true
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
                    record
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  def tag( tags )
         
     | 
| 
      
 54 
     | 
    
         
            +
                    return unless tags and tags.kind_of? Array
         
     | 
| 
      
 55 
     | 
    
         
            +
                    tags.each do |tag|
         
     | 
| 
      
 56 
     | 
    
         
            +
                      tag.split.each {|t| @tags << t if ! @tags.include? t }
         
     | 
| 
      
 57 
     | 
    
         
            +
                    end
         
     | 
| 
      
 58 
     | 
    
         
            +
                    @tags.sort!
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  def untag( tags )
         
     | 
| 
      
 62 
     | 
    
         
            +
                    return unless tags and tags.kind_of? Array
         
     | 
| 
      
 63 
     | 
    
         
            +
                    tags.each do |tag|
         
     | 
| 
      
 64 
     | 
    
         
            +
                      tag.split.each {|t| @tags.delete t }
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end
         
     | 
| 
      
 66 
     | 
    
         
            +
                  end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  def to_s
         
     | 
| 
      
 69 
     | 
    
         
            +
                    puts "id: #{id}, title: #{title}, tags: #{tags}"
         
     | 
| 
      
 70 
     | 
    
         
            +
                  end
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
            end
         
     |