Pratt 1.6.8-x86-linux
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/.exrc +61 -0
- data/.gitignore +6 -0
- data/History.txt +6 -0
- data/Manifest.txt +46 -0
- data/Pratt.gemspec +173 -0
- data/Pratt.mm +1867 -0
- data/README.txt +67 -0
- data/Rakefile +96 -0
- data/TODO +56 -0
- data/VERSION +1 -0
- data/bin/pratt +13 -0
- data/config.rb +19 -0
- data/db/sqlite_databases_go_here +0 -0
- data/db/zips.csv.zip +0 -0
- data/lib/models.rb +8 -0
- data/lib/pratt.rb +308 -0
- data/lib/pratt/core_ext.rb +6 -0
- data/lib/pratt/core_ext/array.rb +20 -0
- data/lib/pratt/core_ext/float.rb +29 -0
- data/lib/pratt/core_ext/nil.rb +5 -0
- data/lib/pratt/core_ext/numeric.rb +9 -0
- data/lib/pratt/core_ext/string.rb +31 -0
- data/lib/pratt/core_ext/time.rb +20 -0
- data/lib/pratt/dialogs.rb +81 -0
- data/lib/pratt/formatting.rb +19 -0
- data/lib/pratt/project_actions.rb +25 -0
- data/lib/pratt/reports.rb +140 -0
- data/models/app.rb +39 -0
- data/models/customer.rb +50 -0
- data/models/invoice.rb +28 -0
- data/models/invoice_whence.rb +18 -0
- data/models/payment.rb +22 -0
- data/models/pratt.rb +15 -0
- data/models/project.rb +89 -0
- data/models/whence.rb +77 -0
- data/models/zip.rb +27 -0
- data/pratt.mm +1875 -0
- data/spec/app_spec.rb +48 -0
- data/spec/array_spec.rb +24 -0
- data/spec/customer_spec.rb +31 -0
- data/spec/fixtures/empty_graph.expectation +8 -0
- data/spec/fixtures/graph.expectation +13 -0
- data/spec/fixtures/proportions.expectation +4 -0
- data/spec/float_spec.rb +24 -0
- data/spec/formatting_spec.rb +7 -0
- data/spec/money_spec.rb +9 -0
- data/spec/nil_class_spec.rb +8 -0
- data/spec/numeric_spec.rb +30 -0
- data/spec/payment_spec.rb +19 -0
- data/spec/pratt_spec.rb +106 -0
- data/spec/project_spec.rb +182 -0
- data/spec/rcov.opts +0 -0
- data/spec/report_action_spec.rb +83 -0
- data/spec/report_spec.rb +205 -0
- data/spec/seed_data.rb +33 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/string_ext_spec.rb +33 -0
- data/spec/whence_spec.rb +54 -0
- data/tasks/pratt.rb +87 -0
- data/templates/model.eruby +12 -0
- data/templates/spec.eruby +8 -0
- data/views/current.eruby +5 -0
- data/views/general-invoice.eruby +538 -0
- data/views/graph.eruby +16 -0
- data/views/invoice.eruby +148 -0
- data/views/main.rb +90 -0
- data/views/pid.eruby +3 -0
- data/views/pop.rb +78 -0
- data/views/proportions.eruby +4 -0
- data/views/raw.eruby +11 -0
- metadata +275 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
class Pratt
|
2
|
+
class Formats
|
3
|
+
module Array
|
4
|
+
##
|
5
|
+
# Turns an array into a sentence
|
6
|
+
#
|
7
|
+
# @param [String] conjunction
|
8
|
+
# @return [String]
|
9
|
+
def to_sentence conjunction = 'and'
|
10
|
+
if self.size >= 2
|
11
|
+
self[0..-2].join(", ") << (self.size > 2 ? ',' : '') << " #{conjunction} #{self.last}"
|
12
|
+
elsif self.size <= 1
|
13
|
+
self.first.to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Array.send :include, Pratt::Formats::Array
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Float
|
2
|
+
def pretty_print
|
3
|
+
"%0.2f"% self
|
4
|
+
end
|
5
|
+
|
6
|
+
def percentage of_total = 1
|
7
|
+
( (self/of_total)*100.0 ).pretty_print << "%"
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_money
|
11
|
+
Money.new(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Money
|
16
|
+
include Comparable
|
17
|
+
|
18
|
+
def <=> other
|
19
|
+
@f <=> other
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize f
|
23
|
+
@f = Float(f)
|
24
|
+
end
|
25
|
+
|
26
|
+
def pretty_print
|
27
|
+
"$#{@f.pretty_print}"
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Pratt
|
2
|
+
module ColorString
|
3
|
+
def or format_alternative
|
4
|
+
return self unless Pratt.color?
|
5
|
+
format_alternative
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Colored
|
10
|
+
def colorize(string, options = {})
|
11
|
+
return self unless Pratt.color?
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
String.send :include, Pratt::ColorString
|
18
|
+
String.send :include, Pratt::Colored
|
19
|
+
|
20
|
+
class String
|
21
|
+
def with_label label
|
22
|
+
"#{padded_to_max(label)} #{self}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# Pad the output string to the maximum Project name
|
27
|
+
def padded_to_max string
|
28
|
+
project_padding = Project.longest_project_name
|
29
|
+
"%#{project_padding}.#{project_padding}s"% string
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Date
|
2
|
+
##
|
3
|
+
# Return "zero hour" of the first day of the, possibly shifted, week.
|
4
|
+
#
|
5
|
+
# @return [Date]
|
6
|
+
def beginning_of_week
|
7
|
+
(self - wday_offset).beginning_of_day
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# Return "last second" of the last day of the, possibly shifted, week.
|
12
|
+
#
|
13
|
+
# @return [Date]
|
14
|
+
def end_of_week
|
15
|
+
(beginning_of_week+6).end_of_day
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class DateTime < Date
|
20
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Pratt
|
2
|
+
def daemonized?
|
3
|
+
!app.pid.blank? and ( cpid.to_i == app.pid )
|
4
|
+
end
|
5
|
+
|
6
|
+
def daemonize!
|
7
|
+
defork do
|
8
|
+
puts "pratt (#{Process.pid.to_s.yellow})"
|
9
|
+
app.pid = Process.pid
|
10
|
+
app.save!
|
11
|
+
|
12
|
+
while daemonized?
|
13
|
+
gui
|
14
|
+
sleep(app.interval)
|
15
|
+
end
|
16
|
+
quit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def gui
|
21
|
+
if Whence.last_unended
|
22
|
+
pop
|
23
|
+
else
|
24
|
+
main
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def detect
|
29
|
+
if self.daemonized?
|
30
|
+
gui
|
31
|
+
else
|
32
|
+
daemonize!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def main
|
38
|
+
reload_and_detect_lock 'main'
|
39
|
+
projects = ([Project.primary, Project.off] | Project.rest).collect(&:name)
|
40
|
+
if Whence.count == 0
|
41
|
+
# first run
|
42
|
+
project = Whence.new(:project => Project.new)
|
43
|
+
current_project_name = project.project.name
|
44
|
+
else
|
45
|
+
project = Whence.last_unended || Whence.last
|
46
|
+
current_project_name = ''
|
47
|
+
end
|
48
|
+
|
49
|
+
if Project.count > 0
|
50
|
+
projects = ([Project.primary, Project.off] | Project.rest).compact.collect(&:name)
|
51
|
+
else
|
52
|
+
projects = []
|
53
|
+
end
|
54
|
+
defork {
|
55
|
+
command = "ruby views/main.rb --projects '#{projects*"','"}' --current '#{current_project_name}'"
|
56
|
+
system command
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def pop
|
61
|
+
reload_and_detect_lock 'pop'
|
62
|
+
self.project = Whence.last_unended.project
|
63
|
+
project_time = project.formatted_time_spent_totals( project.time_spent(scale, when_to) )
|
64
|
+
defork do
|
65
|
+
command = "ruby views/pop.rb --project '#{project.name}' --start '#{project.whences.last_unended.start_at}' --project_time '#{project_time}'"
|
66
|
+
system command
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def reload_and_detect_lock of
|
71
|
+
self.app.reload
|
72
|
+
return if self.app.gui? of
|
73
|
+
self.app.log of
|
74
|
+
end
|
75
|
+
|
76
|
+
def defork &block
|
77
|
+
Process.detach(
|
78
|
+
fork &block
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Pratt
|
2
|
+
def begin
|
3
|
+
self.project.start! when_to
|
4
|
+
end
|
5
|
+
def restart
|
6
|
+
if project?
|
7
|
+
project.restart! when_to
|
8
|
+
else
|
9
|
+
Whence.last_unended.project.restart! when_to
|
10
|
+
end
|
11
|
+
end
|
12
|
+
def end
|
13
|
+
if project?
|
14
|
+
project.stop! when_to
|
15
|
+
else
|
16
|
+
Whence.last_unended.stop! when_to
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def change
|
20
|
+
Whence.last.change! project.name
|
21
|
+
end
|
22
|
+
def destroy
|
23
|
+
project.destroy
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
class Pratt
|
2
|
+
|
3
|
+
# TODO Rename
|
4
|
+
def graph
|
5
|
+
self.template = 'graph'
|
6
|
+
|
7
|
+
if project?
|
8
|
+
@projects = [ project ]
|
9
|
+
else
|
10
|
+
@projects = Project.all
|
11
|
+
end
|
12
|
+
|
13
|
+
process_template!
|
14
|
+
end
|
15
|
+
|
16
|
+
def proportions
|
17
|
+
@primary = @off_total = @rest_total = 0.0
|
18
|
+
self.template = 'proportions'
|
19
|
+
|
20
|
+
if project?
|
21
|
+
@projects = [project]
|
22
|
+
|
23
|
+
@primary = project.time_spent(scale, when_to)
|
24
|
+
@scaled_total = project.time_spent(scale, when_to)
|
25
|
+
else
|
26
|
+
@projects = Project.all
|
27
|
+
|
28
|
+
@projects.each do |proj|
|
29
|
+
@primary = proj.time_spent(scale, when_to) if proj.name == Project.primary.name
|
30
|
+
@off_total = proj.time_spent(scale, when_to) if proj.name == Project.off.name
|
31
|
+
@rest_total += proj.time_spent(scale, when_to) if Project.rest.collect(&:name).include?(proj.name)
|
32
|
+
end
|
33
|
+
@scaled_total = Whence.time_spent(scale, when_to) - @off_total
|
34
|
+
end
|
35
|
+
|
36
|
+
if @primary + @off_total + @rest_total > 0.0
|
37
|
+
process_template!
|
38
|
+
else
|
39
|
+
$stdout.puts "No data to report"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Generate an invoice for a given time period
|
44
|
+
def invoice
|
45
|
+
self.template ||= 'invoice'
|
46
|
+
|
47
|
+
if project?
|
48
|
+
@projects = [project]
|
49
|
+
|
50
|
+
@total = project.amount( project.time_spent(scale, when_to) )
|
51
|
+
else
|
52
|
+
@projects = (Project.all - [Project.primary, Project.off])
|
53
|
+
@projects = @projects.select {|proj| show_all or ( !show_all and proj.time_spent(scale, when_to) != 0.0 ) }
|
54
|
+
|
55
|
+
@total = @projects.inject 0.0 do |total, proj|
|
56
|
+
total += proj.amount( proj.time_spent(scale, when_to) )
|
57
|
+
total
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
@projects.each do |project|
|
62
|
+
puts "<!-- #{project.name} #{project.payment.inspect} -->"
|
63
|
+
end
|
64
|
+
if @total > 0.0
|
65
|
+
process_template!
|
66
|
+
else
|
67
|
+
$stdout.puts "No data to report in Pratt#invoice"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def current
|
72
|
+
self.template = 'current'
|
73
|
+
@last_whence = Whence.last_unended || Whence.last || Whence.new( :end_at => nil, :project => Project.new )
|
74
|
+
|
75
|
+
projects = ([Project.primary, Project.off] | Project.rest).compact
|
76
|
+
@project_names = projects.collect do |proj|
|
77
|
+
|
78
|
+
if @last_whence.end_at.nil? && @last_whence.project.name == proj.name
|
79
|
+
colored_name = proj.name.green
|
80
|
+
else
|
81
|
+
colored_name = proj.name.magenta
|
82
|
+
end
|
83
|
+
"'" << colored_name << "'"
|
84
|
+
end
|
85
|
+
@time_til = ( app.interval - ( Time.now - @last_whence.start_at ) ) if @last_whence.end_at.nil?
|
86
|
+
|
87
|
+
process_template!
|
88
|
+
end
|
89
|
+
|
90
|
+
def raw
|
91
|
+
self.template = 'raw'
|
92
|
+
|
93
|
+
if project?
|
94
|
+
@whences = project.whences.all
|
95
|
+
else
|
96
|
+
case raw_conditions
|
97
|
+
when 'all'
|
98
|
+
@whences = Whence.find raw_conditions.to_sym
|
99
|
+
when /^last$/, 'first'
|
100
|
+
@whences = [Whence.find raw_conditions.to_sym]
|
101
|
+
when /last[\(\s]?(\d+)[\)\s]?/
|
102
|
+
@whences = Whence.all.last($1.to_i)
|
103
|
+
else
|
104
|
+
@whences = Whence.all :conditions => ["start_at > ?", Chronic.parse("today 00:00")]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@whences.sort_by(&:id)
|
108
|
+
process_template!
|
109
|
+
end
|
110
|
+
|
111
|
+
def cpid
|
112
|
+
rubies = %x(ps -C ruby -o pid=,cmd=)
|
113
|
+
pratts = rubies.select do |ruby_process|
|
114
|
+
pid, interp, cmd, args = ruby_process.chomp.split(' ')
|
115
|
+
cmd =~ /pratt/ && args == '-d'
|
116
|
+
end
|
117
|
+
return '' unless pratts.size == 1
|
118
|
+
pratts.first.split(' ').first
|
119
|
+
end
|
120
|
+
|
121
|
+
def pid
|
122
|
+
self.template = 'pid'
|
123
|
+
process_template!
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def output
|
128
|
+
# for testing
|
129
|
+
@__output
|
130
|
+
end
|
131
|
+
|
132
|
+
def process_template!
|
133
|
+
input = File.open(Pratt.root("views", "#{template}.eruby").first).read
|
134
|
+
erubis = Erubis::Eruby.new(input)
|
135
|
+
@__output = erubis.evaluate(self)
|
136
|
+
$stdout.puts @__output
|
137
|
+
nil
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
data/models/app.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
class App < ActiveRecord::Base
|
2
|
+
has_many :logs
|
3
|
+
|
4
|
+
def log which, overwrite = false
|
5
|
+
self.gui = which if (overwrite and gui?(which) ) || gui?('')
|
6
|
+
save!
|
7
|
+
end
|
8
|
+
|
9
|
+
def gui? which = ''
|
10
|
+
match = (self.gui == which)
|
11
|
+
match
|
12
|
+
end
|
13
|
+
|
14
|
+
def unlock
|
15
|
+
self.gui = ''
|
16
|
+
self.save!
|
17
|
+
end
|
18
|
+
|
19
|
+
def interval= intvl
|
20
|
+
write_attribute(:interval, intvl*60)
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def migrate which = :up
|
25
|
+
ActiveRecord::Schema.define do
|
26
|
+
if which == :up
|
27
|
+
create_table :apps do |t|
|
28
|
+
t.integer :pid
|
29
|
+
t.string :gui, :default => ''
|
30
|
+
t.float :interval, :default => 15.0*60
|
31
|
+
end
|
32
|
+
App.create!(:gui => '', :interval => 15.0)
|
33
|
+
elsif which == :down
|
34
|
+
drop_table :apps
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/models/customer.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
class Customer < ActiveRecord::Base
|
2
|
+
has_many :projects
|
3
|
+
has_many :invoices
|
4
|
+
has_one :payment, :as => :billable
|
5
|
+
|
6
|
+
validates_presence_of :name
|
7
|
+
|
8
|
+
def amount
|
9
|
+
payment.rate / 100.0
|
10
|
+
end
|
11
|
+
|
12
|
+
def phone
|
13
|
+
phone = read_attribute(:phone)
|
14
|
+
class << phone
|
15
|
+
def pretty_print sep = '.'
|
16
|
+
if self.blank?
|
17
|
+
""
|
18
|
+
else
|
19
|
+
self.split(/(\d{3})(\d{3})(\d{4})/)[1,3] * sep
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
phone
|
24
|
+
end
|
25
|
+
|
26
|
+
def city_state_zip
|
27
|
+
a_zip = Zip.find_by_zip(self.zip)
|
28
|
+
return "#{a_zip.city}, #{a_zip.ST} #{a_zip.zip}" if a_zip
|
29
|
+
return self.zip
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def migrate which = :up
|
34
|
+
ActiveRecord::Schema.define do
|
35
|
+
if which == :up
|
36
|
+
create_table :customers do |t|
|
37
|
+
t.string :name, :null => false
|
38
|
+
t.string :company_name
|
39
|
+
t.string :address
|
40
|
+
t.string :phone
|
41
|
+
t.string :email
|
42
|
+
t.string :zip, :limit => 5
|
43
|
+
end
|
44
|
+
elsif which == :down
|
45
|
+
drop_table :customers
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|