harvest-reaper 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/assets/alipay.jpg +0 -0
- data/lib/reaper/version.rb +1 -1
- data/lib/reaper.rb +217 -139
- metadata +2 -2
- data/.DS_Store +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18efa0f778351655322cf90cbc38299bbeb9f2bde6f28ff1c92dcaa4aa1224f8
|
4
|
+
data.tar.gz: 1c744e6ca524d2d010669c3fe4037efeea21b9c3f3ba042364f532b7a054b444
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff9d304da8e77067d1cfe89d78320e2711ce9100299c3353651bce26df85795b900e651e1a92e749925864655b338da7cc5adcc759df6f57a61ca12cd5a27781
|
7
|
+
data.tar.gz: a61ece54635d3d7ed46ceb7aa169e91cfa7ec99d54ad84ab6847b27dc3788ec0811c7b1d9d14f816607a86fb7abb2a1fa975030d1ab4c3543e3544481fad17fa
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/assets/alipay.jpg
ADDED
Binary file
|
data/lib/reaper/version.rb
CHANGED
data/lib/reaper.rb
CHANGED
@@ -13,7 +13,6 @@ require 'ruby-progressbar'
|
|
13
13
|
|
14
14
|
module Reaper
|
15
15
|
class Error < StandardError; end
|
16
|
-
# Your code goes here...
|
17
16
|
|
18
17
|
HARVEST_CLIENT_ID = 'c4CbEqRlWx1ziSITWP03BwjN'
|
19
18
|
|
@@ -413,7 +412,7 @@ module Reaper
|
|
413
412
|
to = fri.to_s
|
414
413
|
|
415
414
|
puts "Fetching your time entries from #{from} to #{to}"
|
416
|
-
raw_entries = Reaper.request("time_entries?from=#{from}&to=#{to}")['time_entries']
|
415
|
+
raw_entries = Reaper.request("time_entries?user_id=#{$user_id}&from=#{from}&to=#{to}")['time_entries']
|
417
416
|
|
418
417
|
if raw_entries.empty?
|
419
418
|
puts "Cannot find any time entries in the week of #{from}"
|
@@ -448,14 +447,14 @@ module Reaper
|
|
448
447
|
|
449
448
|
print_time_entries mon, fri, entries
|
450
449
|
|
451
|
-
raw_entries
|
450
|
+
return raw_entries, entries
|
452
451
|
end
|
453
452
|
|
454
453
|
desc "delete DATE/WEEK-ALIAS", "Delete all Harvest time entries in the given week"
|
455
454
|
def delete(date_str)
|
456
455
|
mon, fri = get_week_range_from_date_str date_str
|
457
456
|
|
458
|
-
entries = show date_str
|
457
|
+
entries, _ = show date_str
|
459
458
|
|
460
459
|
if !entries.empty?
|
461
460
|
puts "Delete #{entries.size} entrires from #{mon} to #{fri}? (Y/n)"
|
@@ -482,141 +481,9 @@ module Reaper
|
|
482
481
|
end
|
483
482
|
|
484
483
|
desc "submit DATE/WEEK-ALIAS", "Submit Harvest time entries for you based on the configuration"
|
484
|
+
option :excluded, :type => :string
|
485
485
|
def submit(date_str)
|
486
|
-
|
487
|
-
|
488
|
-
Reaper.check_auth
|
489
|
-
|
490
|
-
abort 'Cannot find Reaper configuration. Please run `reaper config update` first.' unless Reaper.load_config
|
491
|
-
|
492
|
-
existing_entries = show date_str
|
493
|
-
|
494
|
-
if existing_entries && !existing_entries.empty?
|
495
|
-
puts ''
|
496
|
-
abort "You have existing time entries within the specified date range. Reaper submit won't work in this case.\nIf you are sure they can be removed, please run `reaper delete #{date_str}` first."
|
497
|
-
end
|
498
|
-
|
499
|
-
hours_per_day = 8
|
500
|
-
days = 5
|
501
|
-
|
502
|
-
dates = []
|
503
|
-
days.times { |n| dates << mon + n }
|
504
|
-
|
505
|
-
hours = []
|
506
|
-
|
507
|
-
negative_offset_days = 0
|
508
|
-
days.times do |n|
|
509
|
-
# we don't want you to always work less than 8 hours,
|
510
|
-
# 2 is the max number of your less working days
|
511
|
-
is_positive_offset = negative_offset_days <= 4 ? [true, false].sample : true
|
512
|
-
negative_offset_days += 1 unless is_positive_offset
|
513
|
-
|
514
|
-
offset = is_positive_offset ? $config[:daily_positive_offset] : $config[:daily_negative_offset]
|
515
|
-
offset = round_hours(rand() * offset) * (is_positive_offset ? 1 : -1)
|
516
|
-
hours << offset + hours_per_day
|
517
|
-
end
|
518
|
-
|
519
|
-
total_hours = hours.inject(:+)
|
520
|
-
|
521
|
-
tasks = $config[:tasks]
|
522
|
-
|
523
|
-
tasks_hours = 0
|
524
|
-
tasks.each_with_index do |t, i|
|
525
|
-
if i < tasks.size - 1
|
526
|
-
t[:hours] = round_hours(total_hours * t[:percentage])
|
527
|
-
tasks_hours += t[:hours]
|
528
|
-
else
|
529
|
-
t[:hours] = total_hours - tasks_hours
|
530
|
-
end
|
531
|
-
end
|
532
|
-
|
533
|
-
tasks_cpy = tasks.dup
|
534
|
-
|
535
|
-
entries = {}
|
536
|
-
|
537
|
-
hours.each_with_index do |h, i|
|
538
|
-
date = dates[i]
|
539
|
-
|
540
|
-
slots = []
|
541
|
-
slots_num = (h / 0.5).to_i
|
542
|
-
slots_num.times do |n|
|
543
|
-
t = tasks_cpy.sample
|
544
|
-
t[:hours] -= 0.5
|
545
|
-
|
546
|
-
slots << {
|
547
|
-
'project_id' => t[:pid],
|
548
|
-
'task_id' => t[:tid],
|
549
|
-
'spent_date' => date.to_s,
|
550
|
-
'hours' => 0.5
|
551
|
-
}
|
552
|
-
|
553
|
-
if t[:hours] <= 0
|
554
|
-
tasks_cpy.delete t
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
tasks.each do |t|
|
559
|
-
slots_per_task = slots.select do |s|
|
560
|
-
t[:pid] == s['project_id'] && t[:tid] == s['task_id']
|
561
|
-
end
|
562
|
-
|
563
|
-
if !slots_per_task.empty?
|
564
|
-
entry = slots_per_task.first
|
565
|
-
entry['hours'] = slots_per_task.inject(0) { |sum, s| sum + s['hours'] }
|
566
|
-
# puts entry
|
567
|
-
|
568
|
-
daily_tasks = entries[date] || []
|
569
|
-
entries[date] = daily_tasks if daily_tasks.empty?
|
570
|
-
|
571
|
-
daily_tasks << {
|
572
|
-
:project => t[:pname],
|
573
|
-
:project_code => t[:pcode],
|
574
|
-
:task => t[:tname],
|
575
|
-
:client => t[:client],
|
576
|
-
:hours => entry['hours'],
|
577
|
-
:project_id => t[:pid],
|
578
|
-
:task_id => t[:tid],
|
579
|
-
:spent_date => entry['spent_date'],
|
580
|
-
}
|
581
|
-
|
582
|
-
daily_tasks.shuffle!
|
583
|
-
end
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
print_time_entries mon, fri, entries
|
588
|
-
|
589
|
-
puts ''
|
590
|
-
puts "A random set of time entries (from #{mon} to #{fri}) generated, submit now? (Y/n)"
|
591
|
-
confirm = $stdin.gets.chomp
|
592
|
-
abort 'Submit cancelled' unless confirm == 'Y'
|
593
|
-
|
594
|
-
post_data = entries.values.flatten.map do |e|
|
595
|
-
{
|
596
|
-
'user_id' => $user_id,
|
597
|
-
'project_id' => e[:project_id],
|
598
|
-
'task_id' => e[:task_id],
|
599
|
-
'spent_date' => e[:spent_date],
|
600
|
-
'hours' => e[:hours]
|
601
|
-
}
|
602
|
-
end
|
603
|
-
|
604
|
-
progressbar = ProgressBar.create(
|
605
|
-
:title => 'Submitting',
|
606
|
-
:total => post_data.size,
|
607
|
-
:format => '%t %c/%C %B'
|
608
|
-
)
|
609
|
-
|
610
|
-
post_data.each do |e|
|
611
|
-
rsp = post 'time_entries', e
|
612
|
-
if rsp
|
613
|
-
progressbar.increment
|
614
|
-
else
|
615
|
-
abort "Submit request failed. Your submit actions may not be completed. Please run `reaper show #{date_str}` to check."
|
616
|
-
end
|
617
|
-
end
|
618
|
-
|
619
|
-
puts "#{post_data.size} time entries submitted successfully. You can run `reaper show #{date_str}` to check."
|
486
|
+
submit_time_entries date_str, options[:excluded]
|
620
487
|
end
|
621
488
|
|
622
489
|
no_commands do
|
@@ -728,9 +595,213 @@ module Reaper
|
|
728
595
|
table.style = { :all_separators => true }
|
729
596
|
puts table
|
730
597
|
end
|
598
|
+
|
599
|
+
def submit_time_entries(date_str, excluded_weekdays_str)
|
600
|
+
mon, fri = get_week_range_from_date_str date_str
|
601
|
+
|
602
|
+
if excluded_weekdays_str && !excluded_weekdays_str.empty?
|
603
|
+
excluded_weekdays = excluded_weekdays_str
|
604
|
+
.split(',')
|
605
|
+
.map { |s| s.strip.downcase }
|
606
|
+
.uniq
|
607
|
+
|
608
|
+
valid_weekdays = %w(mon tue wed thu fri)
|
609
|
+
unless (excluded_weekdays - valid_weekdays).empty? && excluded_weekdays.size < valid_weekdays.size
|
610
|
+
abort "Argument 'excluded' contains invalid options. Only single or multiple values (comma separated) in #{valid_weekdays} is allowed."
|
611
|
+
end
|
612
|
+
|
613
|
+
excluded_weekdays_offset = excluded_weekdays.map { |d| valid_weekdays.index d }.sort
|
614
|
+
end
|
615
|
+
|
616
|
+
has_excluded = excluded_weekdays_offset != nil && !excluded_weekdays_offset.empty?
|
617
|
+
|
618
|
+
Reaper.check_auth
|
619
|
+
|
620
|
+
abort 'Cannot find Reaper configuration. Please run `reaper config update` first.' unless Reaper.load_config
|
621
|
+
|
622
|
+
_, existing_entries = show date_str
|
623
|
+
|
624
|
+
if existing_entries
|
625
|
+
num = existing_entries.values.map { |v| v.size }.inject(:+)
|
626
|
+
|
627
|
+
if num > 0
|
628
|
+
puts ''
|
629
|
+
|
630
|
+
is_clean_after_excluded = false
|
631
|
+
|
632
|
+
if has_excluded
|
633
|
+
entries = existing_entries.select { |k, v| !(excluded_weekdays_offset.include? (k - mon)) }
|
634
|
+
if entries.values.map { |v| v.size }.inject(:+) > 0
|
635
|
+
abort "You have existing time entries within the specified date range. Reaper submit won't work in this case.\nIf you are sure they can be removed, please run `reaper delete #{date_str}` first."
|
636
|
+
else
|
637
|
+
is_clean_after_excluded = true
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
if !is_clean_after_excluded
|
642
|
+
non_vocation_entries = existing_entries.select do |k, v|
|
643
|
+
case v.size
|
644
|
+
when 0
|
645
|
+
false
|
646
|
+
when 1
|
647
|
+
entry = v.first
|
648
|
+
entry[:client] != 'Time Off' || entry[:hours] != 8
|
649
|
+
else
|
650
|
+
true
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
# all the entries are 8 hours vacation/public holiday/sick leave
|
655
|
+
if non_vocation_entries.empty?
|
656
|
+
case num
|
657
|
+
when 5
|
658
|
+
abort "All 5 days within the specified week have been marked as 'Time Off'. Reaper submit won't work in this case.\nIf you are sure they can be removed, please run `reaper delete #{date_str}` first."
|
659
|
+
else
|
660
|
+
puts "#{num} days within the specified week have been marked as 'Time Off'. Do you want to exclude them and submit time entries for the rest of the days? (Y/n)"
|
661
|
+
excluded_dates = existing_entries.select { |k, v| !v.empty? }.keys
|
662
|
+
excluded_arg = excluded_dates.map { |d| d.strftime('%a') }.join(',')
|
663
|
+
|
664
|
+
confirm = $stdin.gets.chomp
|
665
|
+
abort unless confirm == 'Y'
|
666
|
+
submit_time_entries date_str, excluded_arg
|
667
|
+
return
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
abort "You have existing time entries within the specified date range. Reaper submit won't work in this case.\nIf you are sure they can be removed, please run `reaper delete #{date_str}` first."
|
672
|
+
end
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
hours_per_day = 8
|
677
|
+
days = has_excluded ? 5 - excluded_weekdays_offset.size : 5
|
678
|
+
|
679
|
+
dates = []
|
680
|
+
5.times { |n| dates << mon + n if !has_excluded || !(excluded_weekdays_offset.include? n) }
|
681
|
+
|
682
|
+
hours = []
|
683
|
+
|
684
|
+
negative_offset_days = 0
|
685
|
+
days.times do |_|
|
686
|
+
# we don't want you to always work less than 8 hours,
|
687
|
+
# 2 is the max number of your less working days
|
688
|
+
is_positive_offset = negative_offset_days <= 2 ? [true, false].sample : true
|
689
|
+
negative_offset_days += 1 unless is_positive_offset
|
690
|
+
|
691
|
+
offset = is_positive_offset ? $config[:daily_positive_offset] : $config[:daily_negative_offset]
|
692
|
+
offset = round_hours(rand() * offset) * (is_positive_offset ? 1 : -1)
|
693
|
+
hours << offset + hours_per_day
|
694
|
+
end
|
695
|
+
|
696
|
+
total_hours = hours.inject(:+)
|
697
|
+
|
698
|
+
tasks = $config[:tasks]
|
699
|
+
|
700
|
+
tasks_hours = 0
|
701
|
+
tasks.each_with_index do |t, i|
|
702
|
+
if i < tasks.size - 1
|
703
|
+
t[:hours] = round_hours(total_hours * t[:percentage])
|
704
|
+
tasks_hours += t[:hours]
|
705
|
+
else
|
706
|
+
t[:hours] = total_hours - tasks_hours
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
tasks_cpy = tasks.dup
|
731
711
|
|
712
|
+
entries = {}
|
713
|
+
|
714
|
+
hours.each_with_index do |h, i|
|
715
|
+
date = dates[i]
|
716
|
+
|
717
|
+
slots = []
|
718
|
+
slots_num = (h / 0.5).to_i
|
719
|
+
slots_num.times do |n|
|
720
|
+
t = tasks_cpy.sample
|
721
|
+
t[:hours] -= 0.5
|
722
|
+
|
723
|
+
slots << {
|
724
|
+
'project_id' => t[:pid],
|
725
|
+
'task_id' => t[:tid],
|
726
|
+
'spent_date' => date.to_s,
|
727
|
+
'hours' => 0.5
|
728
|
+
}
|
729
|
+
|
730
|
+
if t[:hours] <= 0
|
731
|
+
tasks_cpy.delete t
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
tasks.each do |t|
|
736
|
+
slots_per_task = slots.select do |s|
|
737
|
+
t[:pid] == s['project_id'] && t[:tid] == s['task_id']
|
738
|
+
end
|
739
|
+
|
740
|
+
if !slots_per_task.empty?
|
741
|
+
entry = slots_per_task.first
|
742
|
+
entry['hours'] = slots_per_task.inject(0) { |sum, s| sum + s['hours'] }
|
743
|
+
# puts entry
|
744
|
+
|
745
|
+
daily_tasks = entries[date] || []
|
746
|
+
entries[date] = daily_tasks if daily_tasks.empty?
|
747
|
+
|
748
|
+
daily_tasks << {
|
749
|
+
:project => t[:pname],
|
750
|
+
:project_code => t[:pcode],
|
751
|
+
:task => t[:tname],
|
752
|
+
:client => t[:client],
|
753
|
+
:hours => entry['hours'],
|
754
|
+
:project_id => t[:pid],
|
755
|
+
:task_id => t[:tid],
|
756
|
+
:spent_date => entry['spent_date'],
|
757
|
+
}
|
758
|
+
|
759
|
+
daily_tasks.shuffle!
|
760
|
+
end
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
print_time_entries mon, fri, entries
|
765
|
+
|
766
|
+
puts ''
|
767
|
+
|
768
|
+
range = "from #{mon} to #{fri}"
|
769
|
+
range << ", #{excluded_weekdays.join(', ')} excluded" if has_excluded
|
770
|
+
|
771
|
+
puts "A random set of time entries (#{range}) generated, submit now? (Y/n)"
|
772
|
+
confirm = $stdin.gets.chomp
|
773
|
+
abort 'Submit cancelled' unless confirm == 'Y'
|
774
|
+
|
775
|
+
post_data = entries.values.flatten.map do |e|
|
776
|
+
{
|
777
|
+
'user_id' => $user_id,
|
778
|
+
'project_id' => e[:project_id],
|
779
|
+
'task_id' => e[:task_id],
|
780
|
+
'spent_date' => e[:spent_date],
|
781
|
+
'hours' => e[:hours]
|
782
|
+
}
|
783
|
+
end
|
784
|
+
|
785
|
+
progressbar = ProgressBar.create(
|
786
|
+
:title => 'Submitting',
|
787
|
+
:total => post_data.size,
|
788
|
+
:format => '%t %c/%C %B'
|
789
|
+
)
|
790
|
+
|
791
|
+
post_data.each do |e|
|
792
|
+
rsp = post 'time_entries', e
|
793
|
+
if rsp
|
794
|
+
progressbar.increment
|
795
|
+
else
|
796
|
+
abort "Submit request failed. Your submit actions may not be completed. Please run `reaper show #{date_str}` to check."
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
puts "#{post_data.size} time entries submitted successfully. You can run `reaper show #{date_str}` to check."
|
801
|
+
end
|
802
|
+
|
732
803
|
# helpers
|
733
|
-
|
804
|
+
|
734
805
|
def num_to_hours(num)
|
735
806
|
num
|
736
807
|
end
|
@@ -781,6 +852,13 @@ module Reaper
|
|
781
852
|
return mon, (mon + 4)
|
782
853
|
end
|
783
854
|
end
|
855
|
+
|
856
|
+
desc "donate", "Buy me a milktea ❤️"
|
857
|
+
def donate
|
858
|
+
puts 'If you think Reaper saves your time, please consider buying the author a milktea ❤️'
|
859
|
+
pay_img = File.join Reaper.root, 'assets/alipay.jpg'
|
860
|
+
`qlmanage -p #{pay_img} 2>/dev/null`
|
861
|
+
end
|
784
862
|
|
785
863
|
desc "version", "Show current Reaper version"
|
786
864
|
def version
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: harvest-reaper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- heartace
|
@@ -102,7 +102,6 @@ executables:
|
|
102
102
|
extensions: []
|
103
103
|
extra_rdoc_files: []
|
104
104
|
files:
|
105
|
-
- ".DS_Store"
|
106
105
|
- ".gitignore"
|
107
106
|
- ".rspec"
|
108
107
|
- ".travis.yml"
|
@@ -111,6 +110,7 @@ files:
|
|
111
110
|
- LICENSE.txt
|
112
111
|
- README.md
|
113
112
|
- Rakefile
|
113
|
+
- assets/alipay.jpg
|
114
114
|
- assets/reaper_config_template.html
|
115
115
|
- bin/.DS_Store
|
116
116
|
- bin/console
|
data/.DS_Store
DELETED
Binary file
|