shiftzilla 0.2.6

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97d7153e749c49c28648b85cd0e2a9704d938b0a462b36d08d506458943d0441
4
+ data.tar.gz: 85af213bfdf7b6ae5c8854dbef2e5f6bd99233edc3f91a292397cf4de5e1f494
5
+ SHA512:
6
+ metadata.gz: 2d00e6716bdffab9b6266021b95aafd6f5d38f809500a12a9f46bdbf9a3225ab5ab6d17cfa0ec61be8a2d138b7e527e7eddfbdb31779f346cc29c562050f6770
7
+ data.tar.gz: ac4499fd1bcf2f048672b528ab422494386f4a2ec18fb2cf0d6e9327d7f5af0ecff3b7bcc7f397b3f7d10ed41104c3207e60777cace213ba71c397ca81b1fedc
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.swp
2
+ *.png
3
+ Gemfile.lock
4
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Shiftzilla
2
+ a.k.a "The tool that we made because Bugzilla lacks any meaingful aggreation reporting."
3
+
4
+ This is a specialized tool for aggregating Bugzilla records in a way that is useful for some development teams. In order to use it:
5
+
6
+ 1. This utility depends on a python-based tool called [python-bugzilla](https://pypi.python.org/pypi/python-bugzilla).
7
+ * Install it so that the `bugzilla` executable is in your $PATH
8
+ * Configure it by running `bugzilla login`
9
+ 2. Next grab this utility from RubyGems:
10
+ * gem install shiftzilla
11
+ * Run any command (like `shiftzilla summary`) to have the utility set up your local $HOME/.shiftzilla directory
12
+ 3. Edit $HOME/.shiftzilla/shiftzilla_cfg.yml to reflect the right organizational info for your teams and groups, plus the saved reports in Bugzilla that you want to draw data from. The utlity expects three tables:
13
+ * One for _all_ team bugs
14
+ * One for bugs filtered by the release that you are tracking
15
+ * One for bugs identified as test blockers by your QE team
16
+
17
+ With all of this done, you can start to run reports (or even set up cron jobs around them):
18
+ * `shiftzilla load` polls bugzilla and stores info in a local SQLite3 database
19
+ * `shiftzilla summary` gives you an overview report in your terminal
20
+ * `shiftzilla build` generates an overall and team-by-team reports that it will push to a web server as static web pages
data/bin/shiftzilla ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'shiftzilla/engine'
4
+ require 'shiftzilla/helpers'
5
+ require 'shiftzilla/version'
6
+ require 'trollop'
7
+
8
+ include Shiftzilla::Engine
9
+ include Shiftzilla::Helpers
10
+
11
+ SUB_COMMANDS = %w{help version load build purge triage}
12
+ Trollop::options do
13
+ version Shiftzilla::VERSION
14
+ banner <<-EOF
15
+ Usage:
16
+ #$0 <command>
17
+
18
+ Commands:
19
+ load
20
+ Queries Bugzilla and loads records into the local DB
21
+ build
22
+ Generates reports based local DB contents
23
+ purge
24
+ Removes today's records from the local DB
25
+ triage
26
+ Generate a report of teams that need to do bug triage
27
+
28
+ Options:
29
+ EOF
30
+ stop_on SUB_COMMANDS
31
+ end
32
+
33
+ cmd = ARGV.shift
34
+ cmd_opts = case cmd
35
+ when 'load'
36
+ Trollop::options do
37
+ banner <<-EOF
38
+ Usage:
39
+ #$0 load <options>
40
+
41
+ Description:
42
+ Runs all of the queries in the 'Queries' portion of the
43
+ config file, and stores info about each retrieved Bugzilla record
44
+ in a local database
45
+
46
+ Options:
47
+ EOF
48
+ end
49
+ when 'build'
50
+ Trollop::options do
51
+ banner <<-EOF
52
+ Usage:
53
+ #$0 build <options>
54
+
55
+ Description:
56
+ Performs aggregations and generates reports based on those,
57
+ which are rsynced to a target location.
58
+
59
+ Options:
60
+ EOF
61
+ opt :local_preview, "Don't publish, just show the report locally", :default => false
62
+ opt :quiet, "For cron use; don't print anything to STDOUT.", :default => false
63
+ end
64
+ when 'purge'
65
+ Trollop::options do
66
+ banner <<-EOF
67
+ Usage:
68
+ #$0 purge <options>
69
+
70
+ Description:
71
+ Purges today's data from the local database
72
+
73
+ Options:
74
+ EOF
75
+ end
76
+ when 'triage'
77
+ Trollop::options do
78
+ banner <<-EOF
79
+ Usage:
80
+ #$0 triage <options>
81
+
82
+ Description:
83
+ Generate a report of bugs with no target release,
84
+ ranked by team / component and age.
85
+
86
+ Options:
87
+ EOF
88
+ end
89
+ when 'help'
90
+ Trollop::educate
91
+ when 'version'
92
+ puts Shiftzilla::VERSION
93
+ exit 0
94
+ end
95
+
96
+ # Check for the config info
97
+ check_config
98
+
99
+ case cmd
100
+ when 'load'
101
+ load_records
102
+ when 'build'
103
+ build_reports(cmd_opts)
104
+ when 'purge'
105
+ purge_records
106
+ when 'triage'
107
+ triage_report
108
+ end
109
+
110
+ exit
@@ -0,0 +1,67 @@
1
+ module Shiftzilla
2
+ class Bug
3
+ attr_reader :id, :first_seen, :last_seen, :test_blocker, :ops_blocker, :owner, :component, :pm_score, :cust_cases, :tgt_release
4
+
5
+ def initialize(bzid,binfo)
6
+ @id = bzid
7
+ @first_seen = binfo[:snapdate]
8
+ @last_seen = binfo[:snapdate]
9
+ @test_blocker = binfo[:test_blocker]
10
+ @ops_blocker = binfo[:ops_blocker]
11
+ @owner = binfo[:owner]
12
+ @summary = binfo[:summary]
13
+ @component = binfo[:component]
14
+ @pm_score = binfo[:pm_score]
15
+ @cust_cases = binfo[:cust_cases]
16
+ @tgt_release = binfo[:tgt_release]
17
+ end
18
+
19
+ def update(binfo)
20
+ @last_seen = binfo[:snapdate]
21
+ @test_blocker = binfo[:test_blocker]
22
+ @ops_blocker = binfo[:ops_blocker]
23
+ @owner = binfo[:owner]
24
+ @summary = binfo[:summary]
25
+ @component = binfo[:component]
26
+ @pm_score = binfo[:pm_score]
27
+ @cust_cases = binfo[:cust_cases]
28
+ @tgt_release = binfo[:tgt_release]
29
+ end
30
+
31
+ def summary
32
+ @summary[0..30].gsub(/\s\w+\s*$/, '...')
33
+ end
34
+
35
+ def age
36
+ (@last_seen - @first_seen).to_i
37
+ end
38
+
39
+ def semver
40
+ parts = @tgt_release.split('.')
41
+ if parts.length == 1
42
+ return @tgt_release
43
+ end
44
+ semver = ''
45
+ first_part = true
46
+ parts.each do |part|
47
+ unless is_number?(part)
48
+ semver += part
49
+ else
50
+ semver += ("%09d" % part).to_s
51
+ end
52
+ # A version like '3.z' gets a middle 0 for sort purposes.
53
+ if first_part and parts.length == 2
54
+ semver += ("%09d" % 0).to_s
55
+ end
56
+ first_part = false
57
+ end
58
+ return semver
59
+ end
60
+
61
+ private
62
+
63
+ def is_number?(val)
64
+ val.to_f.to_s == val.to_s || val.to_i.to_s == val.to_s
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,98 @@
1
+ require 'date'
2
+ require 'shiftzilla/group'
3
+ require 'shiftzilla/release'
4
+ require 'shiftzilla/source'
5
+ require 'shiftzilla/team'
6
+
7
+ module Shiftzilla
8
+ class Config
9
+ attr_reader :teams, :groups, :sources, :releases, :ssh
10
+
11
+ def initialize
12
+ @teams = []
13
+ @groups = []
14
+ @sources = []
15
+ group_map = {}
16
+ cfg_file['Groups'].each do |group|
17
+ gobj = Shiftzilla::Group.new(group)
18
+ @groups << gobj
19
+ group_map[gobj.id] = gobj
20
+ end
21
+ cfg_file['Teams'].each do |team|
22
+ @teams << Shiftzilla::Team.new(team,group_map)
23
+ end
24
+ cfg_file['Sources'].each do |sid,sinfo|
25
+ @sources << Shiftzilla::Source.new(sid,sinfo)
26
+ end
27
+ # Always track a release for bugs with no target release
28
+ @releases = [Shiftzilla::Release.new({ 'name' => '"---"', 'targets' => ['---'] },true)]
29
+ cfg_file['Releases'].each do |release|
30
+ @releases << Shiftzilla::Release.new(release)
31
+ end
32
+ @releases << Shiftzilla::Release.new({ 'name' => 'All', 'targets' => [] },true)
33
+ @ssh = {
34
+ :host => cfg_file['SSH']['host'],
35
+ :path => cfg_file['SSH']['path'],
36
+ :url => cfg_file['SSH']['url'],
37
+ }
38
+ end
39
+
40
+ def earliest_milestone
41
+ milestone_boundaries[:earliest]
42
+ end
43
+
44
+ def latest_milestone
45
+ milestone_boundaries[:latest]
46
+ end
47
+
48
+ def team(tname)
49
+ @teams.select{ |t| t.name == tname }[0]
50
+ end
51
+
52
+ def add_ad_hoc_team(tinfo)
53
+ @teams << Shiftzilla::Team.new(tinfo,{},true)
54
+ end
55
+
56
+ def release(rname)
57
+ @releases.select{ |r| r.name == rname }[0]
58
+ end
59
+
60
+ def release_by_target(tgt)
61
+ return target_map[tgt]
62
+ end
63
+
64
+ private
65
+
66
+ def target_map
67
+ @target_map ||= begin
68
+ tmap = {}
69
+ @releases.each do |release|
70
+ release.targets.each do |target|
71
+ tmap[target] = release
72
+ end
73
+ end
74
+ tmap
75
+ end
76
+ end
77
+
78
+ def milestone_boundaries
79
+ @milestone_boundaries ||= begin
80
+ boundaries = { :earliest => Date.today, :latest => (Date.today - 1800) }
81
+ @releases.each do |release|
82
+ next unless release.uses_milestones?
83
+ ms = release.milestones
84
+ [ms.start,ms.feature_complete,ms.code_freeze,ms.ga].each do |m|
85
+ next if m.date.nil?
86
+ if m.date < boundaries[:earliest]
87
+ boundaries[:earliest] = m.date
88
+ end
89
+ if m.date > boundaries[:latest]
90
+ boundaries[:latest] = m.date
91
+ end
92
+ end
93
+ end
94
+ boundaries
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,183 @@
1
+ require 'highline/import'
2
+ require 'shiftzilla/config'
3
+ require 'shiftzilla/helpers'
4
+ require 'shiftzilla/org_data'
5
+ require 'terminal-table'
6
+ require 'trollop'
7
+
8
+ include Shiftzilla::Helpers
9
+
10
+ module Shiftzilla
11
+ module Engine
12
+ def check_config
13
+ if not File.directory?(SZA_DIR)
14
+ choose do |menu|
15
+ menu.header = 'You don\'t have a Shiftzilla config directory at $HOME/.shiftzilla. Should I create one?'
16
+ menu.prompt = 'Choice?'
17
+ menu.choice(:yes) {
18
+ say('Okay. Creating config directory.')
19
+ Dir.mkdir(SZA_DIR)
20
+ Dir.mkdir(ARCH_DIR)
21
+ }
22
+ menu.choice(:no) {
23
+ say('Okay. Exiting Shiftzilla.')
24
+ exit
25
+ }
26
+ end
27
+ end
28
+ if not File.directory?(ARCH_DIR)
29
+ puts 'You don\'t have an archive directory at $HOME/.shiftzilla/archive. Creating it.'
30
+ Dir.mkdir(ARCH_DIR)
31
+ end
32
+ if not File.exists?(CFG_FILE)
33
+ choose do |menu|
34
+ menu.header = "\nYou don't have a shiftzilla_cfg.yml file in $HOME/.shiftzilla. Should I create one?"
35
+ menu.prompt = 'Choice?'
36
+ menu.choice(:yes) {
37
+ say('Okay. Creating shiftzilla_cfg.yml')
38
+ FileUtils.cp(CFG_TMPL,CFG_FILE)
39
+ }
40
+ menu.choice(:no) {
41
+ say('Okay. Exiting Shiftzilla.')
42
+ exit
43
+ }
44
+ end
45
+ end
46
+ if not File.exists?(DB_FPATH)
47
+ choose do |menu|
48
+ menu.header = "\nYou don't have a shiftzilla.sqlite file in $HOME/.shiftzilla.\nI can create it for you, but it is very important for you to\nconfigure Shiftzilla by puttng the proper settings in\n$HOME/.shiftzilla/shiftzilla_cfg.yml first. Do you want me to proceed with creating the database?"
49
+ menu.prompt = 'Choice?'
50
+ menu.choice(:yes) {
51
+ say('Okay. Creating shiftzilla.sqlite')
52
+ sql_tmpl = File.read(SQL_TMPL)
53
+ tgt_rel_clause = release_clause(releases)
54
+ sql_tmpl.gsub! '$RELEASE_CLAUSE', tgt_rel_clause
55
+ dbh.execute_batch(sql_tmpl)
56
+ dbh.close
57
+ exit
58
+ }
59
+ menu.choice(:no) {
60
+ say('Okay. Exiting Shiftzilla.')
61
+ exit
62
+ }
63
+ end
64
+ end
65
+ end
66
+
67
+ def load_records
68
+ sources.each do |s|
69
+ if s.has_records_for_today?
70
+ puts "Skipping query for #{s.id}; it already has records for today."
71
+ else
72
+ backup_db
73
+ puts "Querying bugzilla for #{s.id}"
74
+ added_count = s.load_records
75
+ puts "Added #{added_count} records to #{s.table}"
76
+ end
77
+ end
78
+ end
79
+
80
+ def purge_records
81
+ sources.each do |s|
82
+ s.purge_records
83
+ end
84
+ puts "Purged #{sources.length} tables."
85
+ end
86
+
87
+ def triage_report
88
+ org_data = Shiftzilla::OrgData.new(shiftzilla_config)
89
+ org_data.populate_releases
90
+ teams = org_data.get_ordered_teams
91
+ no_tgt_rel = shiftzilla_config.releases[0]
92
+
93
+ teams.each do |tname|
94
+ next if tname == '_overall'
95
+ rdata = org_data.get_release_data(tname,no_tgt_rel)
96
+ next if rdata.nil? or rdata.snaps.empty?
97
+ recipients = {}
98
+ team = shiftzilla_config.team(tname)
99
+ unless team.nil? or team.ad_hoc?
100
+ recipients[team.lead] = 1
101
+ recipients[team.group.lead] = 1
102
+ end
103
+ bzids = rdata.snaps.has_key?(latest_snapshot) ? rdata.snaps[latest_snapshot].bug_ids : []
104
+ next if bzids.length == 0
105
+ bugs = rdata.bugs
106
+ table = Terminal::Table.new do |t|
107
+ t << ['URL','Age','Component','Owner','Summary']
108
+ t << :separator
109
+ bzids.sort_by{ |b| [bugs[b].first_seen,bugs[b].component] }.each do |bzid|
110
+ bug = bugs[bzid]
111
+ if team.nil?
112
+ recipients[bug.owner] = 1
113
+ end
114
+ t << [
115
+ bug_url(bzid),
116
+ bug.age,
117
+ bug.component,
118
+ bug.owner,
119
+ bug.summary
120
+ ]
121
+ end
122
+ end
123
+ puts "#{tname}#{team.nil? ? ' Component' : ' Team'} - #{bzids.length} bugs with no Target Release"
124
+ puts "To: #{recipients.keys.sort.join(',')}"
125
+ puts "#{table}\n\n"
126
+ end
127
+ end
128
+
129
+ def build_reports(options)
130
+ org_data = Shiftzilla::OrgData.new(shiftzilla_config)
131
+ org_data.populate_releases
132
+ org_data.build_series
133
+ org_data.generate_reports
134
+ if options[:local_preview]
135
+ org_data.show_local_reports
136
+ else
137
+ org_data.publish_reports(ssh)
138
+ system("rm -rf #{org_data.tmp_dir}")
139
+ unless options[:quiet]
140
+ system("open #{ssh[:url]}")
141
+ end
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ def shiftzilla_config
148
+ @shiftzilla_config ||= Shiftzilla::Config.new
149
+ end
150
+
151
+ def teams
152
+ @teams ||= shiftzilla_config.teams
153
+ end
154
+
155
+ def groups
156
+ @groups ||= shiftzilla_config.groups
157
+ end
158
+
159
+ def sources
160
+ @sources ||= shiftzilla_config.sources
161
+ end
162
+
163
+ def releases
164
+ @releases ||= shiftzilla_config.releases
165
+ end
166
+
167
+ def ssh
168
+ @ssh ||= shiftzilla_config.ssh
169
+ end
170
+
171
+ def component_team_map
172
+ @component_team_map ||= begin
173
+ ctm = {}
174
+ teams.each do |team|
175
+ team.components.each do |component|
176
+ ctm[component] = team
177
+ end
178
+ end
179
+ ctm
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,10 @@
1
+ module Shiftzilla
2
+ class Group
3
+ attr_reader :id, :lead
4
+
5
+ def initialize(ginfo)
6
+ @id = ginfo['id']
7
+ @lead = ginfo['lead']
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,174 @@
1
+ require 'date'
2
+ require 'fileutils'
3
+ require 'haml'
4
+ require 'fileutils'
5
+ require 'sqlite3'
6
+ require 'tmpdir'
7
+ require 'yaml'
8
+
9
+ module Shiftzilla
10
+ module Helpers
11
+ BZ_URL = 'https://bugzilla.redhat.com/show_bug.cgi?id='
12
+ SZA_DIR = File.join(ENV['HOME'],'.shiftzilla')
13
+ ARCH_DIR = File.join(SZA_DIR,'archive')
14
+ CFG_FILE = File.join(SZA_DIR,'shiftzilla_cfg.yml')
15
+ DB_FNAME = 'shiftzilla.sqlite'
16
+ DB_FPATH = File.join(SZA_DIR,DB_FNAME)
17
+ THIS_PATH = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
18
+ HAML_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../template.haml'))
19
+ CFG_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../shiftzilla_cfg.yml.tmpl'))
20
+ SQL_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../shiftzilla.sql.tmpl'))
21
+ VENDOR_DIR = File.expand_path(File.join(File.dirname(THIS_PATH), '../../vendor'))
22
+
23
+ GRAPH_DIMENSIONS = '800x400'
24
+ GRAPH_THEME = {
25
+ :colors => [
26
+ '#268bd2', # Blue
27
+ '#cb4b16', # Orange
28
+ '#859900', # Green
29
+ '#2aa198', # Cyan
30
+ '#d33682', # Magenta
31
+ '#6c71c4', # Violet
32
+ '#b58900', # Yellow
33
+ '#dc322f', # Red
34
+ ],
35
+ :marker_color => '#93a1a1', # Base1
36
+ :font_color => '#586e75', # Base01
37
+ :background_colors => '#fdf6e3', # Base3
38
+ :background_image => nil,
39
+ }
40
+
41
+ def tmp_dir
42
+ @tmp_dir ||= Dir.mktmpdir('shiftzilla-reports-')
43
+ end
44
+
45
+ def cfg_file
46
+ @cfg_file ||= YAML.load_file(CFG_FILE)
47
+ end
48
+
49
+ def dbh
50
+ @dbh ||= SQLite3::Database.new(DB_FPATH)
51
+ end
52
+
53
+ def haml_engine
54
+ @haml_engine ||= Haml::Engine.new(File.read(HAML_TMPL))
55
+ end
56
+
57
+ def new_graph(labels,max_y)
58
+ g = Gruff::Line.new(GRAPH_DIMENSIONS)
59
+ g.theme = GRAPH_THEME
60
+ g.line_width = 2
61
+ g.labels = labels
62
+ g.y_axis_increment = set_axis_increment(max_y)
63
+ g.hide_dots = true
64
+ return g
65
+ end
66
+
67
+ def backup_db
68
+ unless db_backed_up
69
+ today = Date.today.strftime('%Y-%m-%d')
70
+ tpath = File.join(ARCH_DIR,today)
71
+ unless Dir.exists?(tpath)
72
+ Dir.mkdir(tpath)
73
+ end
74
+ apath = ''
75
+ copy_idx = 0
76
+ loop do
77
+ copynum = "%02d" % copy_idx
78
+ apath = File.join(tpath,"#{copynum}-#{DB_FNAME}")
79
+ break unless File.exists?(apath)
80
+ copy_idx += 1
81
+ end
82
+ FileUtils.cp DB_FPATH, apath
83
+ puts "Backed up the database."
84
+ @db_backed_up = true
85
+ end
86
+ end
87
+
88
+ def timestamp
89
+ DateTime.now.to_s
90
+ end
91
+
92
+ def bug_url(bug_id)
93
+ return "#{BZ_URL}#{bug_id}"
94
+ end
95
+
96
+ def set_axis_increment(initial_value)
97
+ case
98
+ when initial_value < 10
99
+ return 1
100
+ when initial_value < 20
101
+ return 2
102
+ when initial_value < 50
103
+ return 5
104
+ when initial_value < 100
105
+ return 10
106
+ when initial_value < 200
107
+ return 20
108
+ when initial_value < 400
109
+ return 25
110
+ else
111
+ return 100
112
+ end
113
+ end
114
+
115
+ def all_bugs_query(snapshot)
116
+ return "SELECT AB.'Bug ID', AB.'Component', AB.'Target Release', AB.'Assignee', AB.'Status', AB.'Summary', AB.'Keywords', AB.'PM Score', AB.'External Bugs' FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')"
117
+ end
118
+
119
+ def component_bugs_query(components,snapshot)
120
+ rclause = list_clause(components)
121
+ rfilter = rclause == '' ? '' : " AND AB.'Component' #{rclause}"
122
+ return "SELECT AB.'Bug ID', AB.'Component', AB.'Target Release', AB.'Assignee', AB.'Status', AB.'Summary', AB.'Keywords', AB.'PM Score', AB.'External Bugs' FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')#{rfilter} ORDER BY AB.'Target Release' DESC"
123
+ end
124
+
125
+ def component_bugs_count(components,snapshot)
126
+ rclause = list_clause(components)
127
+ rfilter = rclause == '' ? '' : " AND AB.'Component' #{rclause}"
128
+ return "SELECT count(*) FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')#{rfilter}"
129
+ end
130
+
131
+ def all_snapshots
132
+ @all_snapshots ||= begin
133
+ all_snapshots = []
134
+ dbh.execute('SELECT DISTINCT Snapshot FROM ALL_BUGS ORDER BY Snapshot ASC') do |row|
135
+ all_snapshots << row[0]
136
+ end
137
+ all_snapshots
138
+ end
139
+ end
140
+
141
+ def latest_snapshot
142
+ all_snapshots[-1]
143
+ end
144
+
145
+ def field_map
146
+ {
147
+ :assigned_to => 'Assignee',
148
+ :component => 'Component',
149
+ :id => 'Bug ID',
150
+ :keywords => 'Keywords',
151
+ :cf_pm_score => 'PM Score',
152
+ :status => 'Status',
153
+ :summary => 'Summary',
154
+ :target_release => 'Target Release',
155
+ :external_bugs => 'External Bugs',
156
+ }
157
+ end
158
+
159
+ private
160
+
161
+ def list_clause(list)
162
+ return '' if list.length == 0
163
+ if list.length > 1
164
+ stmt = list.map{ |t| "'#{t}'" }.join(',')
165
+ return "IN (#{stmt})"
166
+ end
167
+ return "== '#{list[0]}'"
168
+ end
169
+
170
+ def db_backed_up
171
+ @db_backed_up ||= false
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,20 @@
1
+ module Shiftzilla
2
+ class Milestone
3
+ def initialize(mtxt)
4
+ @date = nil
5
+ @stamp = ''
6
+ unless (mtxt.nil? or mtxt == '')
7
+ @date = Date.parse(mtxt)
8
+ @stamp = mtxt
9
+ end
10
+ end
11
+
12
+ def date
13
+ @date
14
+ end
15
+
16
+ def stamp
17
+ @stamp
18
+ end
19
+ end
20
+ end