rtt 0.0.0.1
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/LICENSE +22 -0
- data/Manifest +28 -0
- data/README.rdoc +78 -0
- data/Rakefile +16 -0
- data/USAGE.txt +13 -0
- data/bin/rtt +8 -0
- data/db/rtt.sqlite3 +0 -0
- data/db/test.sqlite3 +0 -0
- data/init.sh +5 -0
- data/lib/rtt.rb +137 -0
- data/lib/rtt/client.rb +25 -0
- data/lib/rtt/cmd_line_interpreter.rb +137 -0
- data/lib/rtt/hash_extensions.rb +86 -0
- data/lib/rtt/project.rb +35 -0
- data/lib/rtt/query_builder.rb +22 -0
- data/lib/rtt/report_generator.rb +190 -0
- data/lib/rtt/storage.rb +19 -0
- data/lib/rtt/task.rb +136 -0
- data/lib/rtt/user.rb +54 -0
- data/lib/rtt/user_configurator.rb +24 -0
- data/rtt.gemspec +50 -0
- data/rtt_report +401 -0
- data/spec/datamapper_spec_helper.rb +1 -0
- data/spec/lib/rtt/task_spec.rb +106 -0
- data/spec/lib/rtt_spec.rb +186 -0
- data/tasks/rtt.rake +66 -0
- data/todo.txt +3 -0
- metadata +207 -0
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            # Hash#deep_merge
         | 
| 3 | 
            +
            # From: http://pastie.textmate.org/pastes/30372, Elliott Hird
         | 
| 4 | 
            +
            # Source: http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
         | 
| 5 | 
            +
            # This file contains extensions to Ruby and other useful snippits of code.
         | 
| 6 | 
            +
            # Time to extend Hash with some recursive merging magic.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
             | 
| 9 | 
            +
            class Hash
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              # Merges self with another hash, recursively.
         | 
| 12 | 
            +
              # 
         | 
| 13 | 
            +
              # This code was lovingly stolen from some random gem:
         | 
| 14 | 
            +
              # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
         | 
| 15 | 
            +
              # 
         | 
| 16 | 
            +
              # Thanks to whoever made it.
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def deep_merge(hash)
         | 
| 19 | 
            +
                target = dup
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                hash.keys.each do |key|
         | 
| 22 | 
            +
                  if hash[key].is_a? Hash and self[key].is_a? Hash
         | 
| 23 | 
            +
                    target[key] = target[key].deep_merge(hash[key])
         | 
| 24 | 
            +
                    next
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                  
         | 
| 27 | 
            +
                  target[key] = hash[key]
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
                
         | 
| 30 | 
            +
                target
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
              # From: http://www.gemtacular.com/gemdocs/cerberus-0.2.2/doc/classes/Hash.html
         | 
| 35 | 
            +
              # File lib/cerberus/utils.rb, line 42
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def deep_merge!(second)
         | 
| 38 | 
            +
                second.each_pair do |k,v|
         | 
| 39 | 
            +
                  if self[k].is_a?(Hash) and second[k].is_a?(Hash)
         | 
| 40 | 
            +
                    self[k].deep_merge!(second[k])
         | 
| 41 | 
            +
                  else
         | 
| 42 | 
            +
                    self[k] = second[k]
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
            #-----------------
         | 
| 49 | 
            +
                    
         | 
| 50 | 
            +
               # cf. http://subtech.g.hatena.ne.jp/cho45/20061122
         | 
| 51 | 
            +
               def deep_merge2(other)
         | 
| 52 | 
            +
                  deep_proc = Proc.new { |k, s, o|
         | 
| 53 | 
            +
                     if s.kind_of?(Hash) && o.kind_of?(Hash)
         | 
| 54 | 
            +
                        next s.merge(o, &deep_proc)
         | 
| 55 | 
            +
                     end
         | 
| 56 | 
            +
                     next o
         | 
| 57 | 
            +
                  }
         | 
| 58 | 
            +
                  merge(other, &deep_proc)
         | 
| 59 | 
            +
               end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 62 | 
            +
               def deep_merge3(second)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  # From: http://www.ruby-forum.com/topic/142809
         | 
| 65 | 
            +
                  # Author: Stefan Rusterholz
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  merger = proc { |key,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
         | 
| 68 | 
            +
                  self.merge(second, &merger)
         | 
| 69 | 
            +
             | 
| 70 | 
            +
               end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
             | 
| 73 | 
            +
               def keep_merge(hash)
         | 
| 74 | 
            +
                  target = dup
         | 
| 75 | 
            +
                  hash.keys.each do |key|
         | 
| 76 | 
            +
                     if hash[key].is_a? Hash and self[key].is_a? Hash
         | 
| 77 | 
            +
                        target[key] = target[key].keep_merge(hash[key])
         | 
| 78 | 
            +
                        next
         | 
| 79 | 
            +
                     end
         | 
| 80 | 
            +
                     #target[key] = hash[key]
         | 
| 81 | 
            +
                     target.update(hash) { |key, *values| values.flatten.uniq }
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                  target
         | 
| 84 | 
            +
               end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            end
         | 
    
        data/lib/rtt/project.rb
    ADDED
    
    | @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -w
         | 
| 2 | 
            +
            module Rtt
         | 
| 3 | 
            +
              class Project
         | 
| 4 | 
            +
                include DataMapper::Resource
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                DEFAULT_NAME = 'default'
         | 
| 7 | 
            +
                DEFAULT_DESCRIPTION = 'Default Project'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                property :id, Serial
         | 
| 10 | 
            +
                property :name, String, :required => true, :unique => true, :default => DEFAULT_NAME
         | 
| 11 | 
            +
                property :description, String, :default => DEFAULT_DESCRIPTION
         | 
| 12 | 
            +
                property :active, Boolean, :default => false
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                has n, :tasks #, :through => Resource
         | 
| 15 | 
            +
                has n, :users, :through => :tasks
         | 
| 16 | 
            +
                belongs_to :client
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                before :valid?, :set_default_client
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def self.default
         | 
| 21 | 
            +
                  first_or_create :active => true
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def activate_with_client(client)
         | 
| 25 | 
            +
                  self.client = client
         | 
| 26 | 
            +
                  self.active = true
         | 
| 27 | 
            +
                  self.save
         | 
| 28 | 
            +
                  self
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def set_default_client
         | 
| 32 | 
            +
                  self.client = Client.default if self.client.nil?
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -w
         | 
| 2 | 
            +
            module Rtt
         | 
| 3 | 
            +
              module QueryBuilder
         | 
| 4 | 
            +
                # Query among all tasks filtering based on parameters.
         | 
| 5 | 
            +
                # 
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                def query options = {}
         | 
| 8 | 
            +
                  Task.all(rtt_build_conditions(options))
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                #private
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def rtt_build_conditions options
         | 
| 14 | 
            +
                  conditions = {}
         | 
| 15 | 
            +
                  conditions[:start_at.gte] = Date.parse(options[:from]) if options[:from]
         | 
| 16 | 
            +
                  conditions[:end_at.lte] = Date.parse(options[:to]) if options[:to]
         | 
| 17 | 
            +
                  conditions[:project] = { :name => options[:project] } if options[:project]
         | 
| 18 | 
            +
                  conditions.deep_merge!({ :project => { :client => { :name => options[:client] } }}) if options[:client]
         | 
| 19 | 
            +
                  conditions
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,190 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -w
         | 
| 2 | 
            +
            module Rtt
         | 
| 3 | 
            +
              module ReportGenerator
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                attr_accessor :data, :different_fixed
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                DEFAULT_FILENAME = 'rtt_report'
         | 
| 8 | 
            +
                FORMATS_ACCEPTED = [ :csv, :pdf ]
         | 
| 9 | 
            +
                REPORT_FIELDS = %w(Client Project Name Date Duration)
         | 
| 10 | 
            +
                FIXED_FIELDS = %w(Client Project)
         | 
| 11 | 
            +
                REPORT_FIELD_OUTPUT = {
         | 
| 12 | 
            +
                  'Client' => Proc.new { |task| task.client.name },
         | 
| 13 | 
            +
                  'Project' => Proc.new { |task| task.project.name },
         | 
| 14 | 
            +
                  'Name' => Proc.new { |task| task.name },
         | 
| 15 | 
            +
                  'Date' => Proc.new { |task| task.date.strftime('%m-%d-%y') },
         | 
| 16 | 
            +
                  'Duration' => Proc.new { |task| task.duration }
         | 
| 17 | 
            +
                }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def fill_user_information(pdf)
         | 
| 20 | 
            +
                  pdf.cell [330, 790],
         | 
| 21 | 
            +
                      :text => current_user.full_name_and_nickname,
         | 
| 22 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 23 | 
            +
                  pdf.cell [330, 770],
         | 
| 24 | 
            +
                      :text => current_user.company,
         | 
| 25 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 26 | 
            +
                  pdf.cell [330, 750],
         | 
| 27 | 
            +
                      :text => current_user.location,
         | 
| 28 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 29 | 
            +
                  pdf.cell [330, 730],
         | 
| 30 | 
            +
                      :text => current_user.address,
         | 
| 31 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 32 | 
            +
                  pdf.cell [330, 710],
         | 
| 33 | 
            +
                      :text => current_user.phone,
         | 
| 34 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 35 | 
            +
                  pdf.cell [330, 690],
         | 
| 36 | 
            +
                      :text => current_user.email,
         | 
| 37 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 38 | 
            +
                  pdf.cell [330, 670],
         | 
| 39 | 
            +
                      :text => current_user.site,
         | 
| 40 | 
            +
                      :width => 225, :padding => 10, :border_width => 0, :align => :right
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def fixed_fields_for_current_data
         | 
| 44 | 
            +
                  @fixed_fields_for_current_data ||= begin
         | 
| 45 | 
            +
                    calculate_fixed_fields_based_on_data
         | 
| 46 | 
            +
                    @data[:fixed_fields].keys + @different_fixed.keys.reject { |key| @different_fixed[key].length > 1 }
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def fixed_value(field)
         | 
| 51 | 
            +
                  if @data[:fixed_fields].include? field
         | 
| 52 | 
            +
                      @data[:fixed_fields][field]
         | 
| 53 | 
            +
                  else
         | 
| 54 | 
            +
                      @different_fixed[field].first
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                def report options = {}
         | 
| 61 | 
            +
                  raise 'Argument must be a valid Hash. Checkout: rtt usage' unless options.is_a?(Hash) || options.keys.empty?
         | 
| 62 | 
            +
                  @different_fixed ||= FIXED_FIELDS.inject({}) { |result, key| result[key] = []; result }
         | 
| 63 | 
            +
                  extension = options.keys.select { |key| FORMATS_ACCEPTED.include?(key) }.first
         | 
| 64 | 
            +
                  path = options[extension]
         | 
| 65 | 
            +
                  fixed_fields = extract_fixed_fields(options)
         | 
| 66 | 
            +
                  fixed_fields_and_values = fixed_fields.inject({}) { |hash, key| hash[key] = options[key.downcase.to_sym]; hash }
         | 
| 67 | 
            +
                  @data = { :fixed_fields => fixed_fields_and_values, :rows => query(options) }
         | 
| 68 | 
            +
                  case extension
         | 
| 69 | 
            +
                    when :pdf
         | 
| 70 | 
            +
                      report_to_pdf path
         | 
| 71 | 
            +
                    when :csv
         | 
| 72 | 
            +
                      raise 'CSV format report not implemented yet'
         | 
| 73 | 
            +
                      report_to_csv path
         | 
| 74 | 
            +
                    else
         | 
| 75 | 
            +
                      raise 'Format not supported. Only csv and pdf are available for the moment.'
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                private
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def calculate_total_hours_and_minutes(data)
         | 
| 83 | 
            +
                  data.inject([0, 0]) do |totals, task|
         | 
| 84 | 
            +
                    total_h, total_m = totals
         | 
| 85 | 
            +
                    if task[4 - fixed_fields_for_current_data.length].match(/^(\d+)h(\d+)m$/)
         | 
| 86 | 
            +
                      total_m += ($2.to_i % 60)
         | 
| 87 | 
            +
                      total_h += ($1.to_i + $2.to_i / 60)
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                    [ total_h, total_m ]
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def extract_fixed_fields(options)
         | 
| 94 | 
            +
                  # remove Duration as we can't filter by that
         | 
| 95 | 
            +
                  REPORT_FIELDS[0..-2].select { |field| options.include?(field.downcase.to_sym) }
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def report_to_csv output_path
         | 
| 99 | 
            +
                  require 'fastercsv'
         | 
| 100 | 
            +
                rescue LoadError
         | 
| 101 | 
            +
                  puts "Missing gem: Fastercsv"
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                 def report_to_pdf output_path
         | 
| 105 | 
            +
                  require 'prawn'
         | 
| 106 | 
            +
                  require 'prawn/layout'
         | 
| 107 | 
            +
                  require "prawn/measurement_extensions"
         | 
| 108 | 
            +
                  columns = REPORT_FIELDS - fixed_fields_for_current_data
         | 
| 109 | 
            +
                  data = @data[:rows].map { |task| task_row_for_fields(task, columns) }
         | 
| 110 | 
            +
                 
         | 
| 111 | 
            +
                  
         | 
| 112 | 
            +
                  total_h, total_m = calculate_total_hours_and_minutes(data)
         | 
| 113 | 
            +
                  report_generator = self
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  pdf = Prawn::Document.new(:page_layout => :portrait,
         | 
| 116 | 
            +
                                 :left_margin => 10.mm,    # different
         | 
| 117 | 
            +
                                 :right_margin => 1.cm,    # units
         | 
| 118 | 
            +
                                 :top_margin => 0.1.dm,    # work
         | 
| 119 | 
            +
                                 :bottom_margin => 0.01.m, # well
         | 
| 120 | 
            +
                                 :page_size => 'A4') do
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    report_generator.fill_user_information(self)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    move_up 140
         | 
| 125 | 
            +
                    font_size 16
         | 
| 126 | 
            +
                    text "RTT Report"
         | 
| 127 | 
            +
                    text "=========="
         | 
| 128 | 
            +
                    move_down 40
         | 
| 129 | 
            +
             | 
| 130 | 
            +
             | 
| 131 | 
            +
            #        text report_generator.current_user.full_name_and_nickname, :align => :right
         | 
| 132 | 
            +
                    #text report_generator.current_user.company, :align => :right
         | 
| 133 | 
            +
                    #text report_generator.current_user.email, :align => :right
         | 
| 134 | 
            +
                    #text report_generator.current_user.address, :align => :right
         | 
| 135 | 
            +
                    #text report_generator.current_user.location, :align => :right
         | 
| 136 | 
            +
                    #text report_generator.current_user.phone, :align => :right
         | 
| 137 | 
            +
                    #text report_generator.current_user.site, :align => :right
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    report_generator.fixed_fields_for_current_data.each do |field|
         | 
| 140 | 
            +
                      text "#{field}: #{report_generator.fixed_value(field)}"
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    move_down 50
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                    table data,
         | 
| 146 | 
            +
                      :headers => columns,
         | 
| 147 | 
            +
                      #:position => :center,
         | 
| 148 | 
            +
                      :position => :left,
         | 
| 149 | 
            +
                      :border_width   => 1,
         | 
| 150 | 
            +
                      :row_colors => [ 'fafafa', 'f0f0f0' ],
         | 
| 151 | 
            +
                      :font_size => 12,
         | 
| 152 | 
            +
                      :padding => 5,
         | 
| 153 | 
            +
                      :align => :left
         | 
| 154 | 
            +
                      #:width =>  535
         | 
| 155 | 
            +
                      #:column_widths => { 1=> 50, 2 => 40, 3 => 30}
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    move_down 20
         | 
| 158 | 
            +
                    text "Total: #{total_h}h#{total_m}m"
         | 
| 159 | 
            +
                    
         | 
| 160 | 
            +
                    # footer
         | 
| 161 | 
            +
            #        page_count.times do |i|
         | 
| 162 | 
            +
                        #go_to_page(i+1)
         | 
| 163 | 
            +
                        #lazy_bounding_box([bounds.right-50, bounds.bottom + 25], :width => 50) {
         | 
| 164 | 
            +
                            #text "#{i+1} / #{page_count}"
         | 
| 165 | 
            +
                        #}.draw
         | 
| 166 | 
            +
                    #end
         | 
| 167 | 
            +
                    number_pages "Page <page> / <total>", [bounds.right - 80, 0]
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                    render_file(output_path || DEFAULT_FILENAME)
         | 
| 170 | 
            +
                  end
         | 
| 171 | 
            +
                rescue LoadError
         | 
| 172 | 
            +
                  puts "Missing gem: prawn, prawn/layout or prawn/measurement_extensions"
         | 
| 173 | 
            +
                end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                def calculate_fixed_fields_based_on_data
         | 
| 176 | 
            +
                  @data[:rows].each do |task|
         | 
| 177 | 
            +
                    (REPORT_FIELDS - @data[:fixed_fields].keys).each do |field|
         | 
| 178 | 
            +
                      value = REPORT_FIELD_OUTPUT[field].call(task)
         | 
| 179 | 
            +
                      @different_fixed[field] << value if FIXED_FIELDS.include?(field) && !@different_fixed[field].include?(value)
         | 
| 180 | 
            +
                    end
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                def task_row_for_fields(task, fields)
         | 
| 185 | 
            +
                  fields.map do |field|
         | 
| 186 | 
            +
                    REPORT_FIELD_OUTPUT[field].call(task)
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
              end
         | 
| 190 | 
            +
            end
         | 
    
        data/lib/rtt/storage.rb
    ADDED
    
    | @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -w
         | 
| 2 | 
            +
            module Rtt
         | 
| 3 | 
            +
              module Storage
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def init(database = :rtt)
         | 
| 6 | 
            +
                  DataMapper.setup(:default, {:adapter => "sqlite3", :database => File.join( File.dirname(__FILE__), '..', '..', "db/#{database.to_s}.sqlite3") })
         | 
| 7 | 
            +
                  migrate unless missing_tables
         | 
| 8 | 
            +
                  DataObjects::Sqlite3.logger = DataMapper::Logger.new(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'log', 'sqlite3.log')), 0)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def migrate #:nodoc:
         | 
| 12 | 
            +
                  DataMapper.auto_migrate!
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def missing_tables
         | 
| 16 | 
            +
                  %W(rtt_projects rtt_users rtt_clients rtt_tasks).reject { |table| DataMapper.repository.storage_exists?(table) }.empty?
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/lib/rtt/task.rb
    ADDED
    
    | @@ -0,0 +1,136 @@ | |
| 1 | 
            +
            #!/usr/bin/ruby -w
         | 
| 2 | 
            +
            module Rtt
         | 
| 3 | 
            +
              class Task
         | 
| 4 | 
            +
                include DataMapper::Resource
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                DEFAULT_NAME = 'Default task'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                property :id, Serial
         | 
| 9 | 
            +
                property :name, Text, :required => true, :default => DEFAULT_NAME
         | 
| 10 | 
            +
                property :date, Date
         | 
| 11 | 
            +
                property :start_at, DateTime
         | 
| 12 | 
            +
                property :end_at, DateTime
         | 
| 13 | 
            +
                property :active, Boolean, :default => false
         | 
| 14 | 
            +
                property :accumulated_spent_time, Float, :default => 0
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                belongs_to :project
         | 
| 17 | 
            +
                has 1, :client, :through => :project
         | 
| 18 | 
            +
                belongs_to :user
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                before :valid?, :set_default_project
         | 
| 21 | 
            +
                before :valid?, :set_default_user
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def self.task(task_name)
         | 
| 24 | 
            +
                  base_attributes = { :name => task_name, :user => Rtt.current_user, :date => Date.today }
         | 
| 25 | 
            +
                  if task_name.nil?
         | 
| 26 | 
            +
                    existing_task = Task.first :active => true
         | 
| 27 | 
            +
                    if existing_task
         | 
| 28 | 
            +
                      existing_task
         | 
| 29 | 
            +
                    else
         | 
| 30 | 
            +
                      base_attributes.merge!(:name => DEFAULT_NAME) if task_name.blank?
         | 
| 31 | 
            +
                      Task.create(base_attributes)
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  elsif (existing_task = Task.first base_attributes.merge({:start_at.gte => Date.today.beginning_of_day})).present?
         | 
| 34 | 
            +
                    existing_task
         | 
| 35 | 
            +
                  else
         | 
| 36 | 
            +
                    Task.create(base_attributes)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def start
         | 
| 41 | 
            +
                  self.start_at = DateTime.now
         | 
| 42 | 
            +
                  self.active = true
         | 
| 43 | 
            +
                  save
         | 
| 44 | 
            +
                  self
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def add_current_spent_time_to_accumulated_spent_time
         | 
| 48 | 
            +
                  self.accumulated_spent_time = self.accumulated_spent_time + self.time_difference_since_start_at
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def clone_task(task)
         | 
| 52 | 
            +
                  task = Task.new
         | 
| 53 | 
            +
                  task.attributes = self.attributes
         | 
| 54 | 
            +
                  task.id = nil
         | 
| 55 | 
            +
                  task
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def duration
         | 
| 59 | 
            +
                  return '-' if end_at.blank?
         | 
| 60 | 
            +
                  convert_to_hour_and_minutes(accumulated_spent_time)
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def set_default_project
         | 
| 64 | 
            +
                  self.project = Project.default if self.project.nil?
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def set_default_user
         | 
| 68 | 
            +
                  self.user = User.default if self.user.nil?
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def stop
         | 
| 72 | 
            +
                  split_task if span_multiple_days?
         | 
| 73 | 
            +
                  finish
         | 
| 74 | 
            +
                  self
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def pause
         | 
| 78 | 
            +
                  split_task if span_multiple_days?
         | 
| 79 | 
            +
                  finish(DateTime.now, true)
         | 
| 80 | 
            +
                  self
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def time_difference_since_start_at
         | 
| 84 | 
            +
                  end_date_or_now = self.end_at ? self.end_at : DateTime.now
         | 
| 85 | 
            +
                  end_date_or_now - self.start_at
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                private
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def convert_to_hour_and_minutes(dif)
         | 
| 91 | 
            +
                  hours, mins = time_diff_in_hours_and_minutes(dif)
         | 
| 92 | 
            +
                  "#{hours}h#{mins}m"
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def finish(end_at = DateTime.now, activation = false)
         | 
| 96 | 
            +
                  self.end_at = end_at
         | 
| 97 | 
            +
                  self.date = end_at.to_date
         | 
| 98 | 
            +
                  self.add_current_spent_time_to_accumulated_spent_time
         | 
| 99 | 
            +
                  self.active = activation
         | 
| 100 | 
            +
                  self.save
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def save_in_between_days_split(date)
         | 
| 104 | 
            +
                  task = clone_task(self)
         | 
| 105 | 
            +
                  task.start_at = date.beginning_of_day
         | 
| 106 | 
            +
                  end_at = date.end_of_day.to_datetime
         | 
| 107 | 
            +
                  task.send(:finish, end_at)
         | 
| 108 | 
            +
                  task.save
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def save_last_task_split(date)
         | 
| 112 | 
            +
                  task = clone_task(self)
         | 
| 113 | 
            +
                  end_at = date.end_of_day.to_datetime
         | 
| 114 | 
            +
                  task.send(:finish, end_at)
         | 
| 115 | 
            +
                  task.save
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                def span_multiple_days?
         | 
| 119 | 
            +
                  self.start_at.day != Date.today.day
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def split_task
         | 
| 123 | 
            +
                  date = Date.today - 1
         | 
| 124 | 
            +
                  while (date > self.start_at)
         | 
| 125 | 
            +
                    save_in_between_days_split(date)
         | 
| 126 | 
            +
                    date -= 1
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                  save_last_task_split(date)
         | 
| 129 | 
            +
                  self.start_at = Date.today.beginning_of_day.to_datetime
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                def time_diff_in_hours_and_minutes(dif)
         | 
| 133 | 
            +
                  Date::day_fraction_to_time(dif)
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
              end
         | 
| 136 | 
            +
            end
         |