redmine_cli 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 00076ae6856ab08c117ae349cddb9612fecf323e
4
- data.tar.gz: 2984d5491b6ad7522e37ae98b1e406c90cb835e5
3
+ metadata.gz: 8ef953c22fdb0728f89e601513e196f11b889b31
4
+ data.tar.gz: 3b90592c3f4c0f1c0718e9d78ca1c0db5e5a3ab6
5
5
  SHA512:
6
- metadata.gz: d3b4cd2a1337400909ab272e2116afc5d8b43f3b5256eec464e9f567cc12a88d25a3b07679d878adec4fc462e341f59498f99be59d55fcb6546e88e99ec8f19c
7
- data.tar.gz: 6b314ee1180f075de75ac480c70c633d7d19c66a0a75689d066ceeeffd36f6eb5126c1a96d4a07d799067829d23efb827649fa8c3cb7c2aadfea1e1d9c216316
6
+ metadata.gz: 01555ab7dbf899ab495bb2ab252bf48f3f925650d4b76e738065e67cc10fb3f89715bd865c7fd645c94a87dd985a500fdd1f630c684fef0f3f6b95c32943ae51
7
+ data.tar.gz: f0721ac52ad8029dcc6fe08c4d7b5cc846c860bb0bd8c6dc6f6485b3bfb1fc10ac396f08f862086e0ff2614059eaad0be3463082fb23dfec5179561ea0506ebf
data/README.md CHANGED
@@ -15,6 +15,8 @@ Why? Because web-browser + mouse sucks
15
15
  redmine conf init
16
16
  redmine issue list [user id]
17
17
  redmine issue show <issue id>
18
+ redmine issue update <issue id> --comment --done 80 --status progress --time 00:30
19
+
18
20
  redmine user find vasya
19
21
  redmine user find 123
20
22
  redmine user find pupkin@yet.another.mail.com
data/bin/console CHANGED
@@ -11,4 +11,6 @@ require 'redmine_cli'
11
11
  # Pry.start
12
12
 
13
13
  require 'irb'
14
+
15
+ ActiveResource::Base.logger = Logger.new(STDOUT)
14
16
  IRB.start
@@ -2,3 +2,4 @@
2
2
  user: nil
3
3
  password: nil
4
4
  site: nil
5
+ locale: en
@@ -7,6 +7,17 @@ en:
7
7
  issue:
8
8
  list: 'Lists issues of user. By default it is you'
9
9
  show: 'More info about issue'
10
+ update: 'Change some issue params, add time entry, comment'
11
+ options:
12
+ show:
13
+ limit: 'Amount of comments (last journals).'
14
+ update:
15
+ done: 'Readiness percentage'
16
+ assign: 'ID of user issue will be assigned to'
17
+ time: 'Add time entry. Formats: HH:MM; M (minutes); H.h (hours in float)'
18
+ status: 'Change status of issue. Search by status name substring'
19
+ comment: 'Leave comment. It will open your text editor.'
20
+ description: 'Change description with your text editor.'
10
21
  conf:
11
22
  init: 'Asks you few question to make your config file useful'
12
23
  user:
@@ -18,8 +29,16 @@ en:
18
29
  enter_user: 'Enter your login to Redmine: '
19
30
  enter_password: 'Enter your password: '
20
31
  enter_site: 'Enter URL to your Redmine: '
32
+ issue:
33
+ update:
34
+ type_comment_here: 'Type your comment here, save file and close'
21
35
 
22
36
  thank_you: 'Thank you!'
23
37
  error_try_again: 'Error! Try again.'
24
38
  error_input_required: 'Input cant be empty.'
25
39
  not_found: 'Not found'
40
+ success: 'success'
41
+ error: 'error'
42
+ wrong_format: 'Wrong format'
43
+ creation_error: 'Creation error'
44
+ enter_object_number: 'Enter object number: '
File without changes
@@ -0,0 +1,69 @@
1
+
2
+ % ljust_value = 15
3
+ % info = { 'ID' => issue.id,
4
+ % 'Subject' => issue.subject,
5
+ % 'Status' => issue.status.name,
6
+ % 'Priority' => issue.priority.name,
7
+ % 'Readiness' => issue.done_ratio + '%',
8
+ % 'Author' => "#{issue.author.name} (#{issue.author.id})" }
9
+ %
10
+ % info['Assigned to'] = "#{issue.assigned_to.name} (#{issue.assigned_to.id})" if issue.assigned_to?
11
+ % info['Project'] = issue.project.name if issue.project?
12
+ % info['Version'] = issue.version.name if issue.version?
13
+ % info['Parent'] = "#{issue.parent.id} - #{issue.parent.reload.subject.cut(80)}" if issue.parent?
14
+ %
15
+ % info.each do |title, value|
16
+ <%= "#{title}:".ljust(ljust_value).yellow %> <%= value + "\n" %>
17
+ % end
18
+ %
19
+ % if issue.children?
20
+ <%= 'Children'.yellow %>:
21
+ % issue.children.each do |i|
22
+ * <%= i.id %> - <%= i.reload.subject.cut(70) %>
23
+ % end
24
+ % end
25
+ %
26
+ % if issue.relations?
27
+ <%= 'Relations'.yellow %>:
28
+ % issue.relations.each do |r|
29
+ % issue_id = r.prefix_options[:issue_id]
30
+ % issue_to_id = r.issue_to_id
31
+ % relation_type = r.relation_type
32
+ * <%= issue_id == issue.id ? 'this' : issue_id %> <%= relation_type %> <%= issue_to_id == issue.id ? 'this' : issue_to_id %>
33
+ % end
34
+
35
+ % end
36
+ %
37
+ % if issue.description? && !issue.description.empty?
38
+
39
+ <%= issue.description %>
40
+
41
+
42
+ % end
43
+ %
44
+ % if issue.journals? && journals_limit > 0
45
+ % journals = []
46
+ % issue.journals.reverse.each do |j|
47
+ % break if journals.size > journals_limit
48
+ % next unless j.notes?
49
+ % journals.push j
50
+ % end
51
+
52
+ % journals.reverse.each do |j|
53
+
54
+ <%= "----------\n".yellow %>
55
+ % journal_info = { 'From' => "#{j.user.name} (#{j.user.id})",
56
+ % 'Created' => j.created_on }
57
+ %
58
+ % journal_info.each do |k, v|
59
+ <%= "#{k}:".ljust(ljust_value).yellow %> <%= v %>
60
+
61
+ % end
62
+
63
+
64
+ <%= j.notes %>
65
+
66
+
67
+ % end
68
+
69
+ % end
File without changes
data/lib/redmine_cli.rb CHANGED
@@ -5,11 +5,12 @@ require 'i18n'
5
5
  I18n.load_path = Dir["#{File.dirname __FILE__}/assets/messages/*"]
6
6
 
7
7
  # helpers
8
- Dir[File.expand_path('../redmine_cli/helpers/*.rb', __FILE__)].each { |f| require f }
8
+ Dir[File.expand_path('../redmine_cli/helpers/**/*.rb', __FILE__)].each { |f| require f }
9
9
 
10
10
  require 'redmine_cli/version'
11
+ require 'redmine_cli/exceptions'
11
12
  require 'redmine_cli/config'
12
- I18n.locale = RedmineCLI::Config['locale'] || :en
13
+ I18n.locale = RedmineCLI::Config['locale']
13
14
 
14
15
  require 'redmine_cli/template_renderer'
15
16
  Dir[File.expand_path('../redmine_cli/subcommands/*.rb', __FILE__)].each { |f| require f }
@@ -0,0 +1,5 @@
1
+ module RedmineCLI
2
+ class UserNotFound < StandardError; end
3
+ class BadInputTime < StandardError; end
4
+ class EmptyList < StandardError; end
5
+ end
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'tempfile'
2
3
 
3
4
  require_relative 'output'
4
5
 
@@ -10,6 +11,59 @@ module RedmineCLI
10
11
  module Input
11
12
  include Helpers::Output
12
13
 
14
+ def ask_from_text_editor(welcome_message = '')
15
+ file = Tempfile.open('redmine_cli') do |f|
16
+ f.write welcome_message
17
+ f
18
+ end
19
+
20
+ editor = Config['editor'] || ENV['EDITOR'] || 'nano'
21
+
22
+ system(editor + ' ' + file.path)
23
+ result = File.read(file)
24
+ file.unlink
25
+
26
+ result
27
+ end
28
+
29
+ #
30
+ # prints names as enumerable list and asks user for element
31
+ #
32
+ def ask_for_object(object_list)
33
+ fail EmptyList if object_list.size.zero?
34
+
35
+ # From 1
36
+ i = 0
37
+ object_list = object_list.map { |obj| [(i += 1), obj] }.to_h
38
+
39
+ print_object_list(object_list)
40
+ puts
41
+ input = ask m(:enter_object_number),
42
+ default: '1',
43
+ limited_to: ->(str) { (1..i).cover? str.to_i }
44
+
45
+ object_list[input.to_i]
46
+ end
47
+
48
+ #
49
+ # Parses time from user's input.
50
+ # Formats: HH:MM; M; H.h
51
+ #
52
+ # @param input [String]
53
+ #
54
+ def parse_time(input)
55
+ fail(BadInputTime) unless input =~ /^\d+[\:\.]?\d*/
56
+
57
+ if input.include?(':')
58
+ h, m = input.split(':').map(&:to_i)
59
+ (60 * h + m) / 60.0
60
+ elsif input.include?('.')
61
+ input.to_f
62
+ else
63
+ input.to_i
64
+ end
65
+ end
66
+
13
67
  #
14
68
  # #ask with :limited_to set to url regexp
15
69
  #
@@ -73,11 +127,13 @@ module RedmineCLI
73
127
  end
74
128
 
75
129
  def fit_in_limit?(input, limit)
76
- fail('limit should be Array or Regexp') unless limit.is_a?(Array) || limit.is_a?(Regexp)
130
+ case limit
131
+ when Array then limit.include?(input)
132
+ when Proc then limit.call(input)
133
+ when Regexp then limit =~ input
77
134
 
78
- return limit.include?(input) if limit.is_a? Array
79
-
80
- limit =~ input
135
+ else fail('limit should be Array or Regexp or Proc')
136
+ end
81
137
  end
82
138
 
83
139
  def read_line
@@ -0,0 +1,86 @@
1
+ module RedmineCLI
2
+ module Helpers
3
+ module Issue
4
+ #
5
+ # some methods for `issue update`
6
+ #
7
+ module Update
8
+ include RedmineRest
9
+ include Helpers::Input
10
+
11
+ private
12
+
13
+ def update_issue(issue)
14
+ @errors = []
15
+
16
+ update_description(issue)
17
+ leave_comment(issue)
18
+ update_done_ratio(issue)
19
+ update_assigned_to(issue)
20
+ update_status(issue)
21
+
22
+ # it should be last, because it creates new object
23
+ add_time_entry_to_issue(issue) if @errors.empty?
24
+
25
+ @errors.empty?
26
+ end
27
+
28
+ def update_done_ratio(issue)
29
+ return unless options[:done]
30
+ issue.done_ratio = options[:done]
31
+ end
32
+
33
+ def update_assigned_to(issue)
34
+ return unless options[:assign]
35
+
36
+ # it can raise exception if there's no such user
37
+ Models::User.find(options[:assign])
38
+ issue.assigned_to_id = options[:assign]
39
+
40
+ rescue ActiveResource::ResourceNotFound
41
+ @errors.push "Assigned: #{m(:not_found)}"
42
+ end
43
+
44
+ def update_status(issue)
45
+ return unless options[:status]
46
+
47
+ found_statuses = Models::IssueStatus.all.filter_by_name_substring(options[:status])
48
+ case found_statuses.size
49
+ when 0 then @errors.push "Status: #{m(:not_found)}"
50
+ when 1 then issue.status_id = found_statuses.first.id
51
+ else issue.status_id = ask_for_object(found_statuses).id
52
+ end
53
+ end
54
+
55
+ def update_description(issue)
56
+ return unless options[:description]
57
+
58
+ issue.description = ask_from_text_editor(issue.description || '')
59
+ end
60
+
61
+ def leave_comment(issue)
62
+ return unless options[:comment]
63
+
64
+ comment = ask_from_text_editor(m('commands.issue.update.type_comment_here'))
65
+ return if comment.strip.empty?
66
+
67
+ issue.notes = comment
68
+ end
69
+
70
+ def add_time_entry_to_issue(issue)
71
+ return unless options[:time]
72
+
73
+ hours = parse_time(options[:time])
74
+ entry = Models::TimeEntry.create issue_id: issue.id,
75
+ hours: hours
76
+
77
+ return if entry.persisted?
78
+ @errors.push "Time: #{m(:creation_error)}"
79
+
80
+ rescue BadInputTime
81
+ @errors.push "Time: #{m(:wrong_format)}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -4,6 +4,18 @@ module RedmineCLI
4
4
  # Helpers for output
5
5
  #
6
6
  module Output
7
+ #
8
+ # for Helpers::Input.
9
+ #
10
+ # Prints keys of `list` and `value.name`
11
+ #
12
+ # @param list [Hash]
13
+ #
14
+ def print_object_list(list)
15
+ key_max_len = list.keys.map { |key| key.to_s.size }.max
16
+ list.each { |k, v| puts "#{k.to_s.ljust(key_max_len)} - #{v.name}" }
17
+ end
18
+
7
19
  #
8
20
  # for Helpers::Input
9
21
  #
@@ -20,11 +20,42 @@ module RedmineCLI
20
20
  end
21
21
 
22
22
  desc 'show <id>', m('desc.issue.show')
23
+ option :limit, aliases: ['-l'], type: :numeric, default: 5, desc: m('desc.issue.options.show.limit')
23
24
  def show(id)
24
- puts erb('issue/show', issue: Models::Issue.find(id))
25
+ puts erb('issue/show', issue: Models::Issue.find(id), journals_limit: options[:limit])
26
+
27
+ rescue ActiveResource::ResourceNotFound # WARNING: it can be raised by associations in template
28
+ puts m(:not_found)
29
+ end
30
+
25
31
  #
26
- # WARNING: it can be raised by associations in template
32
+ # TODO:
33
+ # * estimated time
34
+ # * priority
35
+ # * subject
36
+ # * tracker
37
+ # * project
38
+ # * version
39
+ # * parent (?) - mb it will be in command for relations
27
40
  #
41
+ desc 'update <id>', m('desc.issue.update')
42
+ option :done, type: :numeric, aliases: '-d', desc: m('desc.issue.options.update.done')
43
+ option :assign, type: :numeric, aliases: '-a', desc: m('desc.issue.options.update.assign')
44
+ option :time, type: :string, aliases: '-t', desc: m('desc.issue.options.update.time')
45
+ option :status, type: :string, aliases: '-s', desc: m('desc.issue.options.update.status')
46
+ option :comment, type: :boolean, aliases: '-c', desc: m('desc.issue.options.update.comment')
47
+ option :description, type: :boolean, desc: m('desc.issue.options.update.description')
48
+ def update(id)
49
+ # load helpers inside instance method for better performance
50
+ self.class.include Helpers::Issue::Update
51
+
52
+ issue = Models::Issue.find(id)
53
+ if update_issue(issue) # update_issue will return nil/false if something goes wrong
54
+ puts m(issue.save ? :success : :error)
55
+ else
56
+ @errors.each { |e| puts e }
57
+ end
58
+
28
59
  rescue ActiveResource::ResourceNotFound
29
60
  puts m(:not_found)
30
61
  end
@@ -1,12 +1,14 @@
1
1
  require 'erb'
2
2
  require 'colorize'
3
3
 
4
+ require_relative 'config'
5
+
4
6
  module RedmineCLI
5
7
  #
6
8
  # Renders templates
7
9
  #
8
10
  module TemplateRenderer
9
- @template_directory = File.expand_path('../../assets/templates', __FILE__)
11
+ @template_directory = File.expand_path("../../assets/templates/#{Config.locale}", __FILE__)
10
12
 
11
13
  #
12
14
  # finds template and renders it
@@ -1,3 +1,3 @@
1
1
  module RedmineCLI
2
- VERSION = '0.2.2'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/redmine_cli.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
 
28
28
  spec.add_dependency 'thor', '~> 0.19'
29
29
  spec.add_dependency 'i18n', '~> 0.7'
30
- spec.add_dependency 'redmine_rest', '0.2.0'
30
+ spec.add_dependency 'redmine_rest', '0.4.0'
31
31
  spec.add_dependency 'non_config', '0.1.2'
32
32
  spec.add_dependency 'colorize', '~> 0.7'
33
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redmine_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitriy Non
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-03-07 00:00:00.000000000 Z
11
+ date: 2016-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - '='
102
102
  - !ruby/object:Gem::Version
103
- version: 0.2.0
103
+ version: 0.4.0
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - '='
109
109
  - !ruby/object:Gem::Version
110
- version: 0.2.0
110
+ version: 0.4.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: non_config
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -157,12 +157,14 @@ files:
157
157
  - exe/redmine
158
158
  - lib/assets/default_config.yml
159
159
  - lib/assets/messages/en.yml
160
- - lib/assets/templates/issue/list.erb
161
- - lib/assets/templates/issue/show.erb
162
- - lib/assets/templates/user/find.erb
160
+ - lib/assets/templates/en/issue/list.erb
161
+ - lib/assets/templates/en/issue/show.erb
162
+ - lib/assets/templates/en/user/find.erb
163
163
  - lib/redmine_cli.rb
164
164
  - lib/redmine_cli/config.rb
165
+ - lib/redmine_cli/exceptions.rb
165
166
  - lib/redmine_cli/helpers/input.rb
167
+ - lib/redmine_cli/helpers/issue/update.rb
166
168
  - lib/redmine_cli/helpers/monkey_patching.rb
167
169
  - lib/redmine_cli/helpers/output.rb
168
170
  - lib/redmine_cli/subcommands/conf.rb
@@ -1,26 +0,0 @@
1
-
2
- ID: <%= issue.id %>
3
- Subject: <%= issue.subject %>
4
- Status: <%= issue.status.name %>
5
- Priority: <%= issue.priority.name %>
6
- Readiness: <%= issue.done_ratio + '%' %>
7
- % if issue.project?
8
- Project: <%= issue.project.name %>
9
- % end
10
- % if issue.version?
11
- Version: <%= issue.version.name %>
12
- % end
13
- % if issue.assigned_to?
14
- Assigned to: <%= issue.assigned_to.name %> (<%= issue.assigned_to.id %>)
15
- % end
16
- Author: <%= issue.author.name %> (<%= issue.author.id %>)
17
- % if issue.description? && !issue.description.empty?
18
-
19
- *****
20
-
21
- <%= issue.description %>
22
-
23
-
24
- *****
25
- % end
26
-