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.
Files changed (65) hide show
  1. data/.gitignore +18 -0
  2. data/.passwd_to_unfuddle.example.yml +7 -0
  3. data/README.md +40 -0
  4. data/bin/un +833 -0
  5. data/bin/un.cmd +1 -0
  6. data/confuddle.gemspec +22 -0
  7. data/lib/graft/README.rdoc +138 -0
  8. data/lib/graft/Rakefile +43 -0
  9. data/lib/graft/lib/graft/core_ext/hash.rb +9 -0
  10. data/lib/graft/lib/graft/json.rb +14 -0
  11. data/lib/graft/lib/graft/json/attribute.rb +18 -0
  12. data/lib/graft/lib/graft/json/model.rb +28 -0
  13. data/lib/graft/lib/graft/model.rb +43 -0
  14. data/lib/graft/lib/graft/version.rb +13 -0
  15. data/lib/graft/lib/graft/xml.rb +19 -0
  16. data/lib/graft/lib/graft/xml/attribute.rb +55 -0
  17. data/lib/graft/lib/graft/xml/model.rb +49 -0
  18. data/lib/graft/lib/graft/xml/type.rb +91 -0
  19. data/lib/graft/test/test_helper.rb +38 -0
  20. data/lib/graft/test/unit/core_ext/hash_test.rb +29 -0
  21. data/lib/graft/test/unit/json/attribute_test.rb +51 -0
  22. data/lib/graft/test/unit/json/model_test.rb +86 -0
  23. data/lib/graft/test/unit/xml/attribute_test.rb +161 -0
  24. data/lib/graft/test/unit/xml/model_test.rb +173 -0
  25. data/lib/graft/test/unit/xml/type_test.rb +65 -0
  26. data/lib/unfuzzle/.gitignore +4 -0
  27. data/lib/unfuzzle/README.rdoc +129 -0
  28. data/lib/unfuzzle/Rakefile +39 -0
  29. data/lib/unfuzzle/lib/unfuzzle.rb +87 -0
  30. data/lib/unfuzzle/lib/unfuzzle/comment.rb +37 -0
  31. data/lib/unfuzzle/lib/unfuzzle/component.rb +31 -0
  32. data/lib/unfuzzle/lib/unfuzzle/milestone.rb +54 -0
  33. data/lib/unfuzzle/lib/unfuzzle/person.rb +20 -0
  34. data/lib/unfuzzle/lib/unfuzzle/priority.rb +30 -0
  35. data/lib/unfuzzle/lib/unfuzzle/project.rb +62 -0
  36. data/lib/unfuzzle/lib/unfuzzle/request.rb +75 -0
  37. data/lib/unfuzzle/lib/unfuzzle/response.rb +25 -0
  38. data/lib/unfuzzle/lib/unfuzzle/severity.rb +31 -0
  39. data/lib/unfuzzle/lib/unfuzzle/ticket.rb +156 -0
  40. data/lib/unfuzzle/lib/unfuzzle/ticket_report.rb +29 -0
  41. data/lib/unfuzzle/lib/unfuzzle/time_entry.rb +75 -0
  42. data/lib/unfuzzle/lib/unfuzzle/version.rb +13 -0
  43. data/lib/unfuzzle/test/fixtures/component.xml +8 -0
  44. data/lib/unfuzzle/test/fixtures/components.xml +17 -0
  45. data/lib/unfuzzle/test/fixtures/milestone.xml +12 -0
  46. data/lib/unfuzzle/test/fixtures/milestones.xml +25 -0
  47. data/lib/unfuzzle/test/fixtures/project.xml +17 -0
  48. data/lib/unfuzzle/test/fixtures/projects.xml +35 -0
  49. data/lib/unfuzzle/test/fixtures/severities.xml +24 -0
  50. data/lib/unfuzzle/test/fixtures/severity.xml +8 -0
  51. data/lib/unfuzzle/test/fixtures/ticket.xml +25 -0
  52. data/lib/unfuzzle/test/fixtures/tickets.xml +51 -0
  53. data/lib/unfuzzle/test/test_helper.rb +60 -0
  54. data/lib/unfuzzle/test/unit/unfuzzle/component_test.rb +36 -0
  55. data/lib/unfuzzle/test/unit/unfuzzle/milestone_test.rb +100 -0
  56. data/lib/unfuzzle/test/unit/unfuzzle/priority_test.rb +25 -0
  57. data/lib/unfuzzle/test/unit/unfuzzle/project_test.rb +87 -0
  58. data/lib/unfuzzle/test/unit/unfuzzle/request_test.rb +104 -0
  59. data/lib/unfuzzle/test/unit/unfuzzle/response_test.rb +37 -0
  60. data/lib/unfuzzle/test/unit/unfuzzle/severity_test.rb +36 -0
  61. data/lib/unfuzzle/test/unit/unfuzzle/ticket_test.rb +181 -0
  62. data/lib/unfuzzle/test/unit/unfuzzle_test.rb +39 -0
  63. data/lib/unfuzzle/unfuzzle.gemspec +31 -0
  64. data/lib/version.rb +3 -0
  65. metadata +176 -0
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ log
3
+ log/*.log
4
+ tmp/*
5
+ nbproject
6
+ .tmp_*
7
+ .project
8
+ .idea/**/*
9
+ .idea/*
10
+ .idea
11
+ .rakeTasks
12
+ *.iml
13
+ *.pid
14
+ database.yml
15
+ *.auth
16
+ .generators
17
+ .rakeTasks
18
+ *.gem
@@ -0,0 +1,7 @@
1
+ subdomain: kripton
2
+ username: superman
3
+ password: ihateluther
4
+ default_project_id: 54321 # ticket number 1234 or #1234
5
+ projects_aliases:
6
+ 54321: a # number like a#1234
7
+ 12345: b # number like b#1234
@@ -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