confuddle 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/.gitignore +18 -0
- data/.passwd_to_unfuddle.example.yml +7 -0
- data/README.md +40 -0
- data/bin/un +833 -0
- data/bin/un.cmd +1 -0
- data/confuddle.gemspec +22 -0
- data/lib/graft/README.rdoc +138 -0
- data/lib/graft/Rakefile +43 -0
- data/lib/graft/lib/graft/core_ext/hash.rb +9 -0
- data/lib/graft/lib/graft/json.rb +14 -0
- data/lib/graft/lib/graft/json/attribute.rb +18 -0
- data/lib/graft/lib/graft/json/model.rb +28 -0
- data/lib/graft/lib/graft/model.rb +43 -0
- data/lib/graft/lib/graft/version.rb +13 -0
- data/lib/graft/lib/graft/xml.rb +19 -0
- data/lib/graft/lib/graft/xml/attribute.rb +55 -0
- data/lib/graft/lib/graft/xml/model.rb +49 -0
- data/lib/graft/lib/graft/xml/type.rb +91 -0
- data/lib/graft/test/test_helper.rb +38 -0
- data/lib/graft/test/unit/core_ext/hash_test.rb +29 -0
- data/lib/graft/test/unit/json/attribute_test.rb +51 -0
- data/lib/graft/test/unit/json/model_test.rb +86 -0
- data/lib/graft/test/unit/xml/attribute_test.rb +161 -0
- data/lib/graft/test/unit/xml/model_test.rb +173 -0
- data/lib/graft/test/unit/xml/type_test.rb +65 -0
- data/lib/unfuzzle/.gitignore +4 -0
- data/lib/unfuzzle/README.rdoc +129 -0
- data/lib/unfuzzle/Rakefile +39 -0
- data/lib/unfuzzle/lib/unfuzzle.rb +87 -0
- data/lib/unfuzzle/lib/unfuzzle/comment.rb +37 -0
- data/lib/unfuzzle/lib/unfuzzle/component.rb +31 -0
- data/lib/unfuzzle/lib/unfuzzle/milestone.rb +54 -0
- data/lib/unfuzzle/lib/unfuzzle/person.rb +20 -0
- data/lib/unfuzzle/lib/unfuzzle/priority.rb +30 -0
- data/lib/unfuzzle/lib/unfuzzle/project.rb +62 -0
- data/lib/unfuzzle/lib/unfuzzle/request.rb +75 -0
- data/lib/unfuzzle/lib/unfuzzle/response.rb +25 -0
- data/lib/unfuzzle/lib/unfuzzle/severity.rb +31 -0
- data/lib/unfuzzle/lib/unfuzzle/ticket.rb +156 -0
- data/lib/unfuzzle/lib/unfuzzle/ticket_report.rb +29 -0
- data/lib/unfuzzle/lib/unfuzzle/time_entry.rb +75 -0
- data/lib/unfuzzle/lib/unfuzzle/version.rb +13 -0
- data/lib/unfuzzle/test/fixtures/component.xml +8 -0
- data/lib/unfuzzle/test/fixtures/components.xml +17 -0
- data/lib/unfuzzle/test/fixtures/milestone.xml +12 -0
- data/lib/unfuzzle/test/fixtures/milestones.xml +25 -0
- data/lib/unfuzzle/test/fixtures/project.xml +17 -0
- data/lib/unfuzzle/test/fixtures/projects.xml +35 -0
- data/lib/unfuzzle/test/fixtures/severities.xml +24 -0
- data/lib/unfuzzle/test/fixtures/severity.xml +8 -0
- data/lib/unfuzzle/test/fixtures/ticket.xml +25 -0
- data/lib/unfuzzle/test/fixtures/tickets.xml +51 -0
- data/lib/unfuzzle/test/test_helper.rb +60 -0
- data/lib/unfuzzle/test/unit/unfuzzle/component_test.rb +36 -0
- data/lib/unfuzzle/test/unit/unfuzzle/milestone_test.rb +100 -0
- data/lib/unfuzzle/test/unit/unfuzzle/priority_test.rb +25 -0
- data/lib/unfuzzle/test/unit/unfuzzle/project_test.rb +87 -0
- data/lib/unfuzzle/test/unit/unfuzzle/request_test.rb +104 -0
- data/lib/unfuzzle/test/unit/unfuzzle/response_test.rb +37 -0
- data/lib/unfuzzle/test/unit/unfuzzle/severity_test.rb +36 -0
- data/lib/unfuzzle/test/unit/unfuzzle/ticket_test.rb +181 -0
- data/lib/unfuzzle/test/unit/unfuzzle_test.rb +39 -0
- data/lib/unfuzzle/unfuzzle.gemspec +31 -0
- data/lib/version.rb +3 -0
- metadata +176 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Console Unfuddle
|
2
|
+
================
|
3
|
+
|
4
|
+
Utility for work with unfuddle.com account from console
|
5
|
+
|
6
|
+
1. gem install confuddle
|
7
|
+
2. un install
|
8
|
+
3. Edit HOME_DIR/.passwd_to_unfuddle.yml and fill with your own values
|
9
|
+
4. $ un projects
|
10
|
+
and set for each project aliases "un alias"
|
11
|
+
and set current project "un curr"
|
12
|
+
|
13
|
+
Tasks:
|
14
|
+
|
15
|
+
un addcm NUMBER # add ticket comment
|
16
|
+
un addt NUMBER HOURS COMMENT [DATE] # add time
|
17
|
+
un alias PROJECT_ID ALIAS # set project alias
|
18
|
+
un all [REGEXP] # show all tickets for current project
|
19
|
+
un alla [REGEXP] # show all (in all projects) tickets
|
20
|
+
un assi TICKETS [NEW_ASSIGNEE] # update tickets assingee
|
21
|
+
un at PERIOD # show all times report for account (PERIOD = [tm lm tw lw y [0-9]+])
|
22
|
+
un atm PERIOD # show my times report for account (PERIOD = [tm lm tw lw y [0-9]+])
|
23
|
+
un clear # clear caches
|
24
|
+
un curr ID_OR_ALIAS # set current project id
|
25
|
+
un help [TASK] # Describe available tasks or one specific task
|
26
|
+
un my # show tickets assignee to me for current project
|
27
|
+
un mya # show tickets (in all projects) assignee to me
|
28
|
+
un new ALIAS TITLE [ASSIGNEE] [PRIO] # create ticket
|
29
|
+
un notify PARAMS # Notify about changes with tickets by Gnome-Notify, PARAMS='status,comments'
|
30
|
+
un op TICKET_ID # Open selected ticked from current project in browser
|
31
|
+
un projects # show all projects for your account
|
32
|
+
un show NUMBER [t] # show ticket by number, [t] - show TimeEntries
|
33
|
+
un t PERIOD # show all times report (PERIOD = [tm lm tw lw y [0-9]+])
|
34
|
+
un tm PERIOD # show my times report (PERIOD = [tm lm tw lw y [0-9]+])
|
35
|
+
un upd TICKETS NEW_STATUS # update tickets statuses
|
36
|
+
|
37
|
+
Used gems:
|
38
|
+
|
39
|
+
unfuzzle by Patrick Reagan of Viget Labs
|
40
|
+
grapt by Patrick Reagan
|
data/bin/un
ADDED
@@ -0,0 +1,833 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Console Unfuddle
|
4
|
+
# by Makarchev K. 2011
|
5
|
+
# by Varamashvili M. 2011
|
6
|
+
|
7
|
+
WIN = RUBY_PLATFORM['mingw'] || RUBY_PLATFORM['mswin']
|
8
|
+
MAC = RUBY_PLATFORM['darwin']
|
9
|
+
|
10
|
+
$KCODE='u' if RUBY_VERSION <= "1.8.7"
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'yaml'
|
14
|
+
require 'active_support'
|
15
|
+
require 'active_support/time'
|
16
|
+
|
17
|
+
if(WIN)
|
18
|
+
require 'iconv'
|
19
|
+
require 'win32console'
|
20
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w(.. lib unfuzzle lib unfuzzle) ))
|
21
|
+
else
|
22
|
+
dirname = File.symlink?(__FILE__) ? File.dirname(File.readlink(__FILE__)) : File.dirname(__FILE__)
|
23
|
+
require File.expand_path(File.join(dirname, %w(.. lib unfuzzle lib unfuzzle) ))
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'thor'
|
27
|
+
require 'readline'
|
28
|
+
|
29
|
+
class Unfuddle < Thor
|
30
|
+
|
31
|
+
UNFUDDLE_ENC = "utf-8"
|
32
|
+
WIN_ENC = "cp866"
|
33
|
+
CMD_WIN_ENC = "windows-1251"
|
34
|
+
HOME_DIR = ENV['HOME']
|
35
|
+
PASS_FILE = File.expand_path("#{HOME_DIR}/.passwd_to_unfuddle.yml")
|
36
|
+
CACHED_PEOPLE = "#{HOME_DIR}/.cached_people.yml"
|
37
|
+
CACHED_PROJECTS = "#{HOME_DIR}/.cached_projects.yml"
|
38
|
+
CACHED_TICKETS = "#{HOME_DIR}/.cached_tickets.yml"
|
39
|
+
CACHED_PERIOD = 10.days
|
40
|
+
CACHED_PROJECTS_PERIOD = 10.days
|
41
|
+
CACHED_TICKETS_PERIOD = 60 * 60
|
42
|
+
|
43
|
+
attr_accessor :default_project_id, :projects_aliases
|
44
|
+
|
45
|
+
def initialize(*args)
|
46
|
+
task_name = args[2][:current_task].name rescue 'unknown'
|
47
|
+
|
48
|
+
unless task_name == 'install'
|
49
|
+
|
50
|
+
get_config
|
51
|
+
|
52
|
+
unless me_id
|
53
|
+
say "Error: no matches for me", :red, true
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
|
57
|
+
@started_at = Time.now
|
58
|
+
end
|
59
|
+
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
private
|
65
|
+
def get_config
|
66
|
+
unless File.exists?(PASS_FILE)
|
67
|
+
say "Error: not found #{PASS_FILE}, call `un install`", :red, true
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
|
71
|
+
cfg = YAML.load_file(PASS_FILE)
|
72
|
+
Unfuzzle.subdomain = cfg['subdomain']
|
73
|
+
Unfuzzle.username = cfg['username']
|
74
|
+
Unfuzzle.password = cfg['password']
|
75
|
+
Unfuzzle.use_ssl = true
|
76
|
+
|
77
|
+
self.default_project_id = cfg['default_project_id']
|
78
|
+
self.default_project_id = cached_projects.keys.first if default_project_id.blank? || ([self.default_project_id] & cached_projects.keys).empty?
|
79
|
+
|
80
|
+
self.projects_aliases = cfg['projects_aliases'] || {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_config
|
84
|
+
h = {}
|
85
|
+
h['subdomain'] = Unfuzzle.subdomain
|
86
|
+
h['username'] = Unfuzzle.username
|
87
|
+
h['password'] = Unfuzzle.password
|
88
|
+
|
89
|
+
h['default_project_id'] = self.default_project_id
|
90
|
+
h['projects_aliases'] = self.projects_aliases
|
91
|
+
|
92
|
+
File.open(PASS_FILE, "w+"){ |file| file.puts(h.to_yaml) }
|
93
|
+
end
|
94
|
+
|
95
|
+
if WIN
|
96
|
+
def say(*args)
|
97
|
+
args[0] = Iconv.new(WIN_ENC, UNFUDDLE_ENC).iconv(args.at(0).to_s) rescue args.at(0).to_s
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def enc_input(text)
|
103
|
+
if WIN
|
104
|
+
Iconv.new(UNFUDDLE_ENC, WIN_ENC).iconv(text.to_s)
|
105
|
+
else
|
106
|
+
text
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def enc_cmd(text)
|
111
|
+
if WIN
|
112
|
+
Iconv.new(UNFUDDLE_ENC, CMD_WIN_ENC).iconv(text.to_s)
|
113
|
+
else
|
114
|
+
text
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def color(prio)
|
119
|
+
case prio
|
120
|
+
when 1; :blue
|
121
|
+
when 2; :cyan
|
122
|
+
when 3; nil
|
123
|
+
when 4; :red
|
124
|
+
when 5; :on_red
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def read_text
|
129
|
+
lines = []
|
130
|
+
while line = Readline::readline("> ")
|
131
|
+
lines << line
|
132
|
+
end
|
133
|
+
|
134
|
+
rescue Interrupt => e
|
135
|
+
ensure
|
136
|
+
return enc_input(lines * "\n")
|
137
|
+
end
|
138
|
+
|
139
|
+
# people {id => name}
|
140
|
+
def people
|
141
|
+
@people ||= {}.tap do |h|
|
142
|
+
cached_people.each{|id, data| h[id] = data[:name] }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# cached people
|
147
|
+
def cached_people
|
148
|
+
return @cached_people if @cached_people
|
149
|
+
|
150
|
+
# cached peoples
|
151
|
+
if File.exists?(CACHED_PEOPLE) && File.ctime(CACHED_PEOPLE) > (Time.now - CACHED_PERIOD)
|
152
|
+
@cached_people = YAML.load_file(CACHED_PEOPLE)
|
153
|
+
else
|
154
|
+
people = Unfuzzle::Person.all
|
155
|
+
@cached_people = {}
|
156
|
+
people.each do |person|
|
157
|
+
name = if person.last_name.to_s.blank?
|
158
|
+
person.first_name.to_s.split(" ").reverse.join(" ")
|
159
|
+
else
|
160
|
+
person.last_name.to_s + " " + person.first_name.to_s
|
161
|
+
end
|
162
|
+
name = name.split(" ")
|
163
|
+
if name.size > 0
|
164
|
+
name[1] = name[1].to_s.mb_chars[0].to_s + "."
|
165
|
+
end
|
166
|
+
name = name.join(" ")
|
167
|
+
@cached_people[person.id] = {:name => name, :login => person.username}
|
168
|
+
end
|
169
|
+
File.open(CACHED_PEOPLE, 'w'){|f| f.write(YAML.dump(@cached_people))}
|
170
|
+
end
|
171
|
+
|
172
|
+
@cached_people
|
173
|
+
end
|
174
|
+
|
175
|
+
# my id
|
176
|
+
def me_id
|
177
|
+
@me_id ||= cached_people.detect{|id, data| data[:login] == Unfuzzle.username}.at(0)
|
178
|
+
end
|
179
|
+
|
180
|
+
def me
|
181
|
+
@me ||= people[me_id]
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
def cached_projects
|
186
|
+
return @cached_projects if @cached_projects
|
187
|
+
|
188
|
+
# cached peoples
|
189
|
+
if File.exists?(CACHED_PROJECTS) && File.ctime(CACHED_PROJECTS) > (Time.now - CACHED_PROJECTS_PERIOD)
|
190
|
+
@cached_projects = YAML.load_file(CACHED_PROJECTS)
|
191
|
+
else
|
192
|
+
projects = Unfuzzle.projects
|
193
|
+
@cached_projects = {}
|
194
|
+
projects.each do |project|
|
195
|
+
@cached_projects[project.id] = project.name
|
196
|
+
end
|
197
|
+
File.open(CACHED_PROJECTS, 'w'){|f| f.write(YAML.dump(@cached_projects))}
|
198
|
+
end
|
199
|
+
|
200
|
+
@cached_projects
|
201
|
+
end
|
202
|
+
|
203
|
+
def cached_tickets(params)
|
204
|
+
return @cached_tickets if @cached_tickets
|
205
|
+
if File.exists?(CACHED_TICKETS)
|
206
|
+
yaml = YAML.load_file(CACHED_TICKETS)
|
207
|
+
@cached_tickets = yaml[params.to_s] || []
|
208
|
+
else
|
209
|
+
@cached_tickets = []
|
210
|
+
end
|
211
|
+
@cached_tickets
|
212
|
+
end
|
213
|
+
|
214
|
+
def set_cached_tickets(t, params)
|
215
|
+
yaml = if File.exists?(CACHED_TICKETS)
|
216
|
+
YAML.load_file(CACHED_TICKETS)
|
217
|
+
else
|
218
|
+
{}
|
219
|
+
end
|
220
|
+
|
221
|
+
yaml[params.to_s] = t.map(&:to_hash)
|
222
|
+
|
223
|
+
File.open(CACHED_TICKETS, 'w'){|f| f.write(YAML.dump(yaml))}
|
224
|
+
end
|
225
|
+
|
226
|
+
# number with alias
|
227
|
+
def number_with_a(ticket)
|
228
|
+
number_with_a_by_params(ticket.project_id, ticket.number)
|
229
|
+
end
|
230
|
+
|
231
|
+
def number_with_a_by_params(project_id, number)
|
232
|
+
return number.to_s if project_id == self.default_project_id
|
233
|
+
self.projects_aliases[ project_id ].to_s + number.to_s
|
234
|
+
end
|
235
|
+
|
236
|
+
def find_project_id_by_alias(al)
|
237
|
+
return default_project_id if al.blank?
|
238
|
+
|
239
|
+
res = self.projects_aliases.detect{|k,v| v == al}
|
240
|
+
if res.present?
|
241
|
+
res.first.to_i
|
242
|
+
else
|
243
|
+
default_project_id
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# parse number
|
248
|
+
# f1234 -> [project_id, 1234]
|
249
|
+
# p1235 -> [project_id, 1235]
|
250
|
+
# 1235 -> [default_project_id, 1235]
|
251
|
+
def parse_number(entered_number)
|
252
|
+
entered_number.to_s =~ /(.*?)([0-9]+)/i
|
253
|
+
prj = $1.to_s
|
254
|
+
num = $2.to_s
|
255
|
+
|
256
|
+
if prj.blank?
|
257
|
+
[default_project_id, num.to_i]
|
258
|
+
else
|
259
|
+
[find_project_id_by_alias(prj), num.to_i]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# === Methods ===
|
264
|
+
|
265
|
+
# find persons by regexp, if blank than find me
|
266
|
+
def find_persons(regx = "")
|
267
|
+
return nil if regx.nil?
|
268
|
+
|
269
|
+
res = []
|
270
|
+
if regx == ""
|
271
|
+
return [me_id]
|
272
|
+
else
|
273
|
+
regx = enc_cmd(regx)
|
274
|
+
res = people.select{|id, login| login.mb_chars.downcase =~ /#{regx.mb_chars.strip.downcase}/i}
|
275
|
+
end
|
276
|
+
|
277
|
+
if !res.empty?
|
278
|
+
res.map &:first
|
279
|
+
else
|
280
|
+
say "no peoples matched #{regx}"
|
281
|
+
[]
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def show_projects #!
|
286
|
+
projects = Unfuzzle.projects
|
287
|
+
|
288
|
+
say " ", nil, false
|
289
|
+
say "id".ljust(10), nil, false
|
290
|
+
say "name".ljust(20), nil, false
|
291
|
+
say 'title'.ljust(20), nil, false
|
292
|
+
say "disk-usage".ljust(20), nil, false
|
293
|
+
say "alias"
|
294
|
+
|
295
|
+
projects.each do |project|
|
296
|
+
say project.id.to_i == self.default_project_id ? " * " : " ", nil, false
|
297
|
+
say project.id.to_s.ljust(10), :yellow, false
|
298
|
+
say project.slug.ljust(20), :green, false
|
299
|
+
say project.name.ljust(20), nil, false
|
300
|
+
say project.disk_usage.to_s.ljust(20), nil, false
|
301
|
+
say projects_aliases[project.id].to_s
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Ticket Heads
|
306
|
+
|
307
|
+
def render_ticket_head(ticket)
|
308
|
+
color = color(ticket.priority)
|
309
|
+
say number_with_a(ticket).to_s.ljust(9), :yellow, false
|
310
|
+
say ticket.title.mb_chars[0..60].ljust(65), color, false
|
311
|
+
say ticket.status.ljust(14), color, false
|
312
|
+
say people[ticket.reporter_id].to_s.mb_chars.ljust(15), color, false
|
313
|
+
say people[ticket.assignee_id].to_s.mb_chars.ljust(15), color, false
|
314
|
+
say ticket.hours.to_s, color, true
|
315
|
+
end
|
316
|
+
|
317
|
+
def render_ticket_heads(tickets)
|
318
|
+
groups = {}
|
319
|
+
tickets.each do |ticket|
|
320
|
+
groups[ticket.status] ||= []
|
321
|
+
groups[ticket.status] << ticket
|
322
|
+
end
|
323
|
+
|
324
|
+
groups.each do |group, tickets|
|
325
|
+
tickets = tickets.sort_by{|t| t.number.to_i}
|
326
|
+
tickets.each{|ticket| render_ticket_head(ticket) }
|
327
|
+
say ''
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def render_ticket_heads_by_people(t)
|
332
|
+
groups = {}
|
333
|
+
t.each do |ticket|
|
334
|
+
groups[ticket.assignee_id] ||= []
|
335
|
+
groups[ticket.assignee_id] << ticket
|
336
|
+
end
|
337
|
+
|
338
|
+
groups.each do |ass_id, tickets|
|
339
|
+
say "============== #{people[ass_id].to_s.mb_chars} ================ (#{tickets.size}) tickets", :on_red, true
|
340
|
+
render_ticket_heads tickets
|
341
|
+
say ''
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def render_ticket_heads_by_project(t)
|
346
|
+
groups = {}
|
347
|
+
t.each do |ticket|
|
348
|
+
groups[ticket.project_id] ||= []
|
349
|
+
groups[ticket.project_id] << ticket
|
350
|
+
end
|
351
|
+
|
352
|
+
groups.each do |project_id, tickets|
|
353
|
+
say "Project => #{cached_projects[project_id]}: ", :yellow, true
|
354
|
+
render_ticket_heads_by_people tickets
|
355
|
+
say ''
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def show_all_active_tickets(name = nil, all_projects = false) #!
|
360
|
+
ass_ids = find_persons(name == "" ? nil : name) || []
|
361
|
+
t = Unfuzzle::Ticket.all_by_dinamic_report(all_projects ? nil : [default_project_id], false)
|
362
|
+
t.reject!{|ticket| !ass_ids.blank? && !ass_ids.include?(ticket.assignee_id) }
|
363
|
+
render_ticket_heads_by_project t
|
364
|
+
end
|
365
|
+
|
366
|
+
def show_my_tickets(all_projects = false) #!
|
367
|
+
t = Unfuzzle::Ticket.all_by_dinamic_report(all_projects ? nil : [default_project_id], true)
|
368
|
+
render_ticket_heads_by_project t
|
369
|
+
end
|
370
|
+
|
371
|
+
# TimeEntry
|
372
|
+
|
373
|
+
def render_times(times, sum_only = false)
|
374
|
+
times = times.sort_by{|t| t.date.to_s }
|
375
|
+
|
376
|
+
sum = 0
|
377
|
+
title_present = false
|
378
|
+
|
379
|
+
times.each do |time|
|
380
|
+
unless sum_only
|
381
|
+
#say time.ticket_id.to_s.ljust(8), :yellow, false
|
382
|
+
title_present = true if time.title.present?
|
383
|
+
say time.title.to_s.mb_chars[0..30].ljust(34), :yellow, false if title_present
|
384
|
+
say time.description.mb_chars[0..50].ljust(54), nil, false
|
385
|
+
say (time.hours.to_s + " h.").ljust(9), nil, false
|
386
|
+
say people[time.person_id].to_s.mb_chars.ljust(13), nil, false
|
387
|
+
say time.date, nil
|
388
|
+
end
|
389
|
+
|
390
|
+
sum += time.hours.to_f
|
391
|
+
end
|
392
|
+
|
393
|
+
say ''.ljust(34), nil, false if title_present
|
394
|
+
say ''.ljust(54), nil, false
|
395
|
+
say (sum.to_s + " h.").ljust(10), :red, true
|
396
|
+
|
397
|
+
sum
|
398
|
+
end
|
399
|
+
|
400
|
+
def render_people_times(times, sum_only = false)
|
401
|
+
groups = {}
|
402
|
+
times.each do |time|
|
403
|
+
groups[time.person_id] ||= []
|
404
|
+
groups[time.person_id] << time
|
405
|
+
end
|
406
|
+
|
407
|
+
groups.each do |group, times|
|
408
|
+
say "============== #{people[group].to_s.mb_chars} ================", :on_red, true
|
409
|
+
render_times(times, sum_only)
|
410
|
+
say ''
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def filter_times(times, for_id = nil)
|
415
|
+
times.select do |time|
|
416
|
+
time.person_id == for_id
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def times_report(me = false, period = 0, sum_only=false)
|
421
|
+
p = periods(period)
|
422
|
+
t = Unfuzzle::TimeEntry.time_invested(default_project_id, p.at(0), p.at(1))
|
423
|
+
t = filter_times(t, me_id) if me
|
424
|
+
render_people_times(t, sum_only)
|
425
|
+
end
|
426
|
+
|
427
|
+
def all_times_report(me = false, period = 0, sum_only=false)
|
428
|
+
p = periods(period)
|
429
|
+
t = Unfuzzle::TimeEntry.all_time_invested(p.at(0), p.at(1))
|
430
|
+
t = filter_times(t, me_id) if me
|
431
|
+
render_people_times(t, sum_only)
|
432
|
+
end
|
433
|
+
|
434
|
+
def periods(period)
|
435
|
+
case period
|
436
|
+
when 'ty' then [Time.now.beginning_of_year, Time.now]
|
437
|
+
when 'ly' then [(Time.now.beginning_of_year-1.seconds).beginning_of_year, (Time.now.beginning_of_year-1.seconds)]
|
438
|
+
when 'tm' then [Time.now.beginning_of_month, Time.now]
|
439
|
+
when 'lm' then [(Time.now.beginning_of_month-1.seconds).beginning_of_month, (Time.now.beginning_of_month-1.seconds)]
|
440
|
+
when 'tw' then [Time.now.beginning_of_week, Time.now]
|
441
|
+
when 'lw' then [(Time.now.beginning_of_week-1.seconds).beginning_of_week, (Time.now.beginning_of_week-1.seconds)]
|
442
|
+
when 'y' then [(Time.new - 1.day).beginning_of_day, ((Time.new - 1.day).end_of_day)]
|
443
|
+
else [Time.now - period.to_i.days, Time.now]
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
# Ticket
|
448
|
+
|
449
|
+
def render_comment(cm)
|
450
|
+
say people[cm.author_id].to_s.mb_chars, :on_blue, false
|
451
|
+
say " " + cm.created_at.to_s, nil, true
|
452
|
+
say cm.body.mb_chars, nil, true
|
453
|
+
end
|
454
|
+
|
455
|
+
def render_ticket(t, comments = [], time_entries = [])
|
456
|
+
color = color(t.priority)
|
457
|
+
|
458
|
+
say number_with_a(t).to_s, :on_red
|
459
|
+
say t.title.to_s.mb_chars, :on_blue
|
460
|
+
say cached_projects[t.project_id].to_s, color, true
|
461
|
+
say t.status, color
|
462
|
+
say t.priority_name, color
|
463
|
+
say people[t.reporter_id].to_s + " => " + people[t.assignee_id].to_s, color, true
|
464
|
+
say t.description.to_s.mb_chars, color, true
|
465
|
+
say t.hours.to_s + " h.", color
|
466
|
+
|
467
|
+
if !time_entries.blank?
|
468
|
+
say ''
|
469
|
+
say "Time Entries: ", :blue, true
|
470
|
+
render_times(time_entries)
|
471
|
+
end
|
472
|
+
|
473
|
+
if !comments.blank?
|
474
|
+
say ''
|
475
|
+
say 'Comments: ', :on_red, true
|
476
|
+
comments.each{|cm| render_comment(cm) }
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def show_ticket(num, opt)
|
481
|
+
prj, number = parse_number(num)
|
482
|
+
t, cm = Unfuzzle::Ticket.find_first_by_project_id_and_number_with_comments(prj, number)
|
483
|
+
times = (opt == "t") ? Unfuzzle::TimeEntry.all_for_ticket(t) : []
|
484
|
+
render_ticket(t, cm, times)
|
485
|
+
end
|
486
|
+
|
487
|
+
def open_ticket_in_browser(num)
|
488
|
+
prj, number = parse_number(num)
|
489
|
+
url = "https://#{Unfuzzle.subdomain}.unfuddle.com/a#/projects/#{prj}/tickets/by_number/#{number}"
|
490
|
+
if(WIN)
|
491
|
+
system("start #{url}")
|
492
|
+
elsif(MAC)
|
493
|
+
system("open #{url}")
|
494
|
+
else
|
495
|
+
system("xdg-open #{url}")
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
#
|
500
|
+
def update_tickets(tickets, new_status)
|
501
|
+
tickets = tickets.split(",").map &:strip
|
502
|
+
if !tickets.blank? && !new_status.blank?
|
503
|
+
ts = tickets.map{|num|
|
504
|
+
prj, number = parse_number(num)
|
505
|
+
Unfuzzle::Ticket.find_first_by_project_id_and_number(prj, number) rescue nil
|
506
|
+
}.compact
|
507
|
+
say "Update tickets #{ts.map(&:number) * ','} to status #{new_status}"
|
508
|
+
|
509
|
+
ts.each do |t|
|
510
|
+
t.status = new_status
|
511
|
+
t.update
|
512
|
+
render_ticket(t)
|
513
|
+
end
|
514
|
+
else
|
515
|
+
say "no one tickets"
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
def update_tickets_assignee(tickets, new_assignee)
|
520
|
+
tickets = tickets.split(",").map &:strip
|
521
|
+
if !tickets.blank?
|
522
|
+
ts = tickets.map{|num|
|
523
|
+
prj, number = parse_number(num)
|
524
|
+
Unfuzzle::Ticket.find_first_by_project_id_and_number(prj, number) rescue nil
|
525
|
+
}.compact
|
526
|
+
|
527
|
+
# find person
|
528
|
+
ass_ids = find_persons(new_assignee)
|
529
|
+
exit 1 if ass_ids.size < 1
|
530
|
+
|
531
|
+
person = people[ass_ids.first]
|
532
|
+
|
533
|
+
say "Update tickets #{ts.map(&:number) * ','} to assignee #{person}"
|
534
|
+
|
535
|
+
ts.each do |t|
|
536
|
+
t.assignee_id = ass_ids.first
|
537
|
+
t.update
|
538
|
+
render_ticket(t)
|
539
|
+
end
|
540
|
+
else
|
541
|
+
say "no one tickets"
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def new_ticket(title, assignee = "", priority = 3)
|
546
|
+
# find user
|
547
|
+
ass_id = find_persons(assignee).first
|
548
|
+
|
549
|
+
prio = priority.blank? ? 3 : priority.to_i
|
550
|
+
|
551
|
+
t = Unfuzzle::Ticket.new
|
552
|
+
t.title = enc_cmd(title)
|
553
|
+
t.assignee_id = ass_id
|
554
|
+
t.reporter_id = find_persons.first # me
|
555
|
+
t.priority = prio
|
556
|
+
t.project_id = default_project_id
|
557
|
+
|
558
|
+
say "Now enter description"
|
559
|
+
t.description = read_text
|
560
|
+
|
561
|
+
say "Create ticket: ", nil, true
|
562
|
+
say t.title, nil, true
|
563
|
+
say t.description, nil, true
|
564
|
+
say people[t.assignee_id]
|
565
|
+
say ''
|
566
|
+
|
567
|
+
t.create
|
568
|
+
render_ticket(t)
|
569
|
+
end
|
570
|
+
|
571
|
+
def add_comment(number)
|
572
|
+
prj, number = parse_number(number)
|
573
|
+
t, cms = Unfuzzle::Ticket.find_first_by_project_id_and_number_with_comments(prj, number)
|
574
|
+
|
575
|
+
say "Enter body of comment: ", :red, true
|
576
|
+
text = read_text
|
577
|
+
|
578
|
+
cm = Unfuzzle::Comment.new
|
579
|
+
cm.body = text
|
580
|
+
cm.author_id = me_id
|
581
|
+
|
582
|
+
cm.create(t.project_id, t.id)
|
583
|
+
|
584
|
+
cms += [cm]
|
585
|
+
|
586
|
+
render_ticket(t, cms)
|
587
|
+
end
|
588
|
+
|
589
|
+
def add_time(number, hours, comment = "", date = "")
|
590
|
+
prj, number = parse_number(number)
|
591
|
+
t, cms = Unfuzzle::Ticket.find_first_by_project_id_and_number_with_comments(prj, number)
|
592
|
+
|
593
|
+
time = Unfuzzle::TimeEntry.new
|
594
|
+
time.description = enc_cmd(comment)
|
595
|
+
time.hours = hours
|
596
|
+
time.ticket_id = t.id
|
597
|
+
time.date = date.blank? ? Time.now.strftime("%Y-%m-%d") : date
|
598
|
+
time.person_id = me_id
|
599
|
+
|
600
|
+
time.create(t.project_id, t.id)
|
601
|
+
|
602
|
+
times = Unfuzzle::TimeEntry.all_for_ticket(t)
|
603
|
+
|
604
|
+
t.hours = (t.hours.to_f + time.hours.to_f).to_s
|
605
|
+
render_ticket(t, cms, times)
|
606
|
+
end
|
607
|
+
|
608
|
+
def set_current(number)
|
609
|
+
if cached_projects.keys.include?(number.to_i)
|
610
|
+
self.default_project_id = number.to_i
|
611
|
+
set_config
|
612
|
+
say "set current project to #{number}"
|
613
|
+
say ''
|
614
|
+
show_projects
|
615
|
+
elsif projects_aliases.values.include?(number.to_s) # alias?
|
616
|
+
self.default_project_id = projects_aliases.detect{|k,v| v == number.to_s}.first rescue self.default_project_id
|
617
|
+
set_config
|
618
|
+
say "set current project to #{number}"
|
619
|
+
say ''
|
620
|
+
show_projects
|
621
|
+
else
|
622
|
+
say "bad number #{number}, should be in #{cached_projects.keys.inspect}"
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def set_alias(number, _alias)
|
627
|
+
if cached_projects.keys.include?(number.to_i)
|
628
|
+
self.projects_aliases[number.to_i] = _alias
|
629
|
+
set_config
|
630
|
+
say "set project #{number} alias to #{_alias}"
|
631
|
+
say
|
632
|
+
show_projects
|
633
|
+
else
|
634
|
+
say "bad number #{number}, should be in #{cached_projects.keys.inspect}"
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
def notify_periodic(params = '')
|
639
|
+
t = Unfuzzle::Ticket.all_by_dinamic_report(nil, true).map(&:to_hash)
|
640
|
+
|
641
|
+
# options
|
642
|
+
show_new = true # показывать новые созданные тикеты
|
643
|
+
show_statuses = true # показывать изменения статусов тикетов
|
644
|
+
show_closed = true # показывать какие были закрыты
|
645
|
+
all_tickets = true # учитывать ли все тикеты (не только привязанные ко мне)
|
646
|
+
my_tickets = true # делать нотификацию только тикетов которые созданы мной или на меня
|
647
|
+
all_projects = true # тикеты брать со всех проектов или с текущего
|
648
|
+
|
649
|
+
params = [show_new,show_statuses,show_closed,all_tickets,my_tickets,all_projects]
|
650
|
+
|
651
|
+
t = Unfuzzle::Ticket.all_by_dinamic_report(all_projects ? nil : [default_project_id], !all_tickets).map(&:to_hash)
|
652
|
+
|
653
|
+
cached = cached_tickets(params)
|
654
|
+
|
655
|
+
if cached.size == 0
|
656
|
+
say "first time: caching ..."
|
657
|
+
set_cached_tickets(t, params)
|
658
|
+
return
|
659
|
+
end
|
660
|
+
|
661
|
+
if my_tickets
|
662
|
+
cached = cached.select{|c| c['assignee-id'] == me_id || c['reporter-id'] == me_id }
|
663
|
+
t = t.select{|c| c['assignee-id'] == me_id || c['reporter-id'] == me_id }
|
664
|
+
end
|
665
|
+
|
666
|
+
cached_h = {}
|
667
|
+
t_h = {}
|
668
|
+
|
669
|
+
# into hash
|
670
|
+
cached.each{|c| k = [c['project-id'], c['number']]; cached_h[k] = c }
|
671
|
+
t.each{|c| k = [c['project-id'], c['number']]; t_h[k] = c }
|
672
|
+
|
673
|
+
|
674
|
+
message = []
|
675
|
+
|
676
|
+
# new tickets
|
677
|
+
if show_new && (t_h.keys - cached_h.keys).present?
|
678
|
+
(t_h.keys - cached_h.keys).each{|key| message << "New ticket #{number_with_a_by_params(key[0], key[1])}: '#{t_h[key]['summary']}'"}
|
679
|
+
end
|
680
|
+
|
681
|
+
# change status tickets
|
682
|
+
chanched = []
|
683
|
+
t_h.each{|key, value| chanched << key if cached_h[key] && value['status'] != cached_h[key]['status']}
|
684
|
+
|
685
|
+
if show_statuses && chanched.present?
|
686
|
+
chanched.each{|key| message << "#{number_with_a_by_params(key[0], key[1])}: '#{t_h[key]['summary']}' => #{t_h[key]['status']}"}
|
687
|
+
end
|
688
|
+
|
689
|
+
# closed tickets
|
690
|
+
if show_closed && (cached_h.keys - t_h.keys).present?
|
691
|
+
(cached_h.keys - t_h.keys).each{|key| message << "Closed ticket #{number_with_a_by_params(key[0], key[1])}: '#{cached_h[key]['summary']}'"}
|
692
|
+
end
|
693
|
+
|
694
|
+
message = message * "\n"
|
695
|
+
notify_message(message)
|
696
|
+
|
697
|
+
set_cached_tickets(t, params)
|
698
|
+
end
|
699
|
+
|
700
|
+
def notify_message(message)
|
701
|
+
return if message.empty?
|
702
|
+
if WIN
|
703
|
+
say message
|
704
|
+
|
705
|
+
elsif MAC
|
706
|
+
system("growlnotify --name 'confuddle' -s -m '#{message}'")
|
707
|
+
|
708
|
+
else
|
709
|
+
system("dbus-launch notify-send -t 5000 \"Unfuddle notification\" \"#{message}\"")
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
public
|
714
|
+
|
715
|
+
desc "projects", "show all projects for your account"
|
716
|
+
def projects
|
717
|
+
show_projects
|
718
|
+
end
|
719
|
+
|
720
|
+
desc "all [REGEXP]", "show all tickets for current project"
|
721
|
+
def all(regexp = "")
|
722
|
+
show_all_active_tickets(regexp)
|
723
|
+
end
|
724
|
+
|
725
|
+
desc "my", "show tickets assignee to me for current project"
|
726
|
+
def my
|
727
|
+
show_my_tickets
|
728
|
+
end
|
729
|
+
|
730
|
+
desc "alla [REGEXP]", "show all (in all projects) tickets"
|
731
|
+
def alla(regexp = "")
|
732
|
+
show_all_active_tickets(regexp, true)
|
733
|
+
end
|
734
|
+
|
735
|
+
desc "mya", "show tickets (in all projects) assignee to me"
|
736
|
+
def mya
|
737
|
+
show_my_tickets true
|
738
|
+
end
|
739
|
+
|
740
|
+
desc "show NUMBER [t]", "show ticket by number, [t] - show TimeEntries"
|
741
|
+
def show(number, opt = "")
|
742
|
+
show_ticket(number, opt)
|
743
|
+
end
|
744
|
+
|
745
|
+
# Upd
|
746
|
+
desc "upd TICKETS NEW_STATUS", "update tickets statuses"
|
747
|
+
def upd(tickets, new_status)
|
748
|
+
update_tickets(tickets, new_status)
|
749
|
+
end
|
750
|
+
|
751
|
+
desc "assi TICKETS [NEW_ASSIGNEE]", "update tickets assingee"
|
752
|
+
def assi(tickets, new_assignee = "")
|
753
|
+
update_tickets_assignee(tickets, new_assignee)
|
754
|
+
end
|
755
|
+
|
756
|
+
desc "clear", "clear caches"
|
757
|
+
def clear
|
758
|
+
require 'fileutils'
|
759
|
+
FileUtils.rm(CACHED_PEOPLE) rescue nil
|
760
|
+
FileUtils.rm(CACHED_PROJECTS) rescue nil
|
761
|
+
say "cached cleared"
|
762
|
+
end
|
763
|
+
|
764
|
+
# Times
|
765
|
+
desc "tm PERIOD", "show my times report (PERIOD = [tm lm tw lw y [0-9]+])"
|
766
|
+
def tm(period = 0, sum_only='')
|
767
|
+
times_report(true, period, sum_only == 's')
|
768
|
+
end
|
769
|
+
|
770
|
+
desc "t PERIOD", "show all times report (PERIOD = [tm lm tw lw y [0-9]+])"
|
771
|
+
def t(period = 0, sum_only='')
|
772
|
+
times_report(false, period, sum_only == 's')
|
773
|
+
end
|
774
|
+
|
775
|
+
desc "atm PERIOD", "show my times report for account (PERIOD = [tm lm tw lw y [0-9]+])"
|
776
|
+
def atm(period = 0, sum_only='')
|
777
|
+
all_times_report(true, period, sum_only == 's')
|
778
|
+
end
|
779
|
+
|
780
|
+
desc "at PERIOD", "show all times report for account (PERIOD = [tm lm tw lw y [0-9]+])"
|
781
|
+
def at(period = 0, sum_only='')
|
782
|
+
all_times_report(false, period, sum_only == 's')
|
783
|
+
end
|
784
|
+
|
785
|
+
# Add
|
786
|
+
desc "addt NUMBER HOURS COMMENT [DATE]", "add time"
|
787
|
+
def addt(ticket_number, hours, comment, date = nil)
|
788
|
+
add_time(ticket_number, hours, comment, date)
|
789
|
+
end
|
790
|
+
|
791
|
+
desc "addcm NUMBER", "add ticket comment"
|
792
|
+
def addcm(ticket_number)
|
793
|
+
add_comment(ticket_number)
|
794
|
+
end
|
795
|
+
|
796
|
+
desc "new TITLE [ASSIGNEE] [PRIO]", "create ticket"
|
797
|
+
def new(title, assignee = "", priority = 3)
|
798
|
+
new_ticket(title, assignee, priority)
|
799
|
+
end
|
800
|
+
|
801
|
+
desc "curr ID_OR_ALIAS", "set current project id"
|
802
|
+
def curr(current)
|
803
|
+
set_current(current)
|
804
|
+
end
|
805
|
+
|
806
|
+
desc "alias PROJECT_ID ALIAS", "set project alias"
|
807
|
+
def alias(project_id, _alias)
|
808
|
+
set_alias(project_id, _alias)
|
809
|
+
end
|
810
|
+
|
811
|
+
desc "op TICKET_ID", "Open selected ticked from current project in browser"
|
812
|
+
def op(ticket_id)
|
813
|
+
open_ticket_in_browser(ticket_id)
|
814
|
+
end
|
815
|
+
|
816
|
+
desc "notify PARAMS", "Notify about changes with tickets by Gnome-Notify, PARAMS='status,comments'"
|
817
|
+
def notify(params = '')
|
818
|
+
notify_periodic params
|
819
|
+
end
|
820
|
+
|
821
|
+
desc "install", "copy sample config into home"
|
822
|
+
def install
|
823
|
+
require 'fileutils'
|
824
|
+
file = File.expand_path(File.join(File.dirname(__FILE__), %w(.. .passwd_to_unfuddle.example.yml) ))
|
825
|
+
FileUtils.cp(file, PASS_FILE)
|
826
|
+
say("Config copyed!", :green)
|
827
|
+
say("Edit file #{PASS_FILE}!", :red)
|
828
|
+
end
|
829
|
+
|
830
|
+
default_task(:my) # by default
|
831
|
+
end
|
832
|
+
|
833
|
+
Unfuddle.start
|