shiftzilla 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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